Android NDK开发详解连接性之WLAN 直连[对等连接或 P2P]概览


使用 WLAN 直连 (P2P) 技术,可以让具备相应硬件的 Android 4.0(API 级别 14)或更高版本设备在没有中间接入点的情况下,通过 WLAN 进行直接互联。使用这些 API,您可以实现支持 WLAN P2P 的设备间相互发现和连接,从而获得比蓝牙连接更远距离的高速连接通信效果。对于多人游戏或照片共享等需要在用户之间共享数据的应用而言,这一技术非常有用。

WLAN P2P API 包含以下主要部分:

支持您发现、请求,以及连接到对等设备的方法(在 WifiP2pManager 类中定义)。

支持您获知 WifiP2pManager 方法调用成功与否的侦听器。调用 WifiP2pManager 方法时,每个方法均可收到作为参数传入的特定侦听器。

通知您 WLAN P2P 框架检测到的特定事件(例如连接断开或新发现对等设备)的 Intent。

通常,您可以一起使用 API 的这三个主要组件。例如,您可以为针对 discoverPeers() 的调用提供 WifiP2pManager.ActionListener,这样您便可以通过 ActionListener.onSuccess() 和 ActionListener.onFailure() 方法来接收通知。如果 discoverPeers() 方法发现对等设备列表已经更改,则还将广播 WIFI_P2P_PEERS_CHANGED_ACTION Intent。

API 概览

WifiP2pManager 类提供的方法使您可以在设备上与 WLAN 硬件交互,以执行发现和连接对等设备等操作。可执行的操作如下:

表 1. WLAN P2P 方法

在这里插入图片描述

WifiP2pManager 方法使您可以在侦听器中进行传递,以便 WLAN P2P 框架可以向您的 Activity 通知通话状态。下表介绍可用的侦听器接口和使用侦听器的相应 WifiP2pManager 方法调用:

表 2. WLAN P2P 侦听器

在这里插入图片描述

WLAN P2P API 定义当发生特定 WLAN P2P 事件时会广播的 Intent,例如发现新的对等设备时,或设备的 WLAN 状态更改时。您可以通过创建处理这些 Intent 的广播接收器,在应用中注册接收这些 Intent:

表 3. WLAN P2P Intent

在这里插入图片描述

为 WLAN P2P Intent 创建广播接收器

广播接收器允许您通过 Android 系统接收 Intent 广播,以便您的应用对您感兴趣的事件作出响应。创建广播接收器以处理 WLAN P2P Intent 的基本步骤如下:

创建扩展 BroadcastReceiver 类的类。对于类的构造函数,您很可能希望具备 WifiP2pManager、WifiP2pManager.Channel,以及此广播接收器将在其中注册的 Activity 的参数。这使广播接收器可以向 Activity 发送更新,访问 WLAN 硬件并获得通信通道(如果需要)。

在广播接收器中,查看您感兴趣的 Intent onReceive()。根据接收到的 Intent,执行任何必要操作。例如,如果广播接收器接收到 WIFI_P2P_PEERS_CHANGED_ACTION Intent,则您可以调用 requestPeers() 方法,以获得当前所发现对等设备的列表。

以下代码展示如何创建典型的广播接收器。广播接收器以 WifiP2pManager 对象和 Activity 作为参数,并在接收到 Intent 时,使用这两个类恰当地执行所需操作:

Kotlin

/**
 * A BroadcastReceiver that notifies of important Wi-Fi p2p events.
 */
class WiFiDirectBroadcastReceiver(
        private val manager: WifiP2pManager,
        private val channel: WifiP2pManager.Channel,
        private val activity: MyWifiActivity
) : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val action: String = intent.action
        when (action) {
            WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
                // Check to see if Wi-Fi is enabled and notify appropriate activity
            }
            WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
                // Call WifiP2pManager.requestPeers() to get a list of current peers
            }
            WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
                // Respond to new connection or disconnections
            }
            WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {
                // Respond to this device's wifi state changing
            }
        }
    }
}

Java

/**
 * A BroadcastReceiver that notifies of important Wi-Fi p2p events.
 */
public class WiFiDirectBroadcastReceiver extends BroadcastReceiver {

    private WifiP2pManager mManager;
    private Channel mChannel;
    private MyWiFiActivity mActivity;

    public WiFiDirectBroadcastReceiver(WifiP2pManager manager, Channel channel,
            MyWifiActivity activity) {
        super();
        this.mManager = manager;
        this.mChannel = channel;
        this.mActivity = 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 Q,以下广播 Intent 已从粘性变为非粘性:

WIFI_P2P_CONNECTION_CHANGED_ACTION
应用可使用 requestConnectionInfo()、requestNetworkInfo() 或 requestGroupInfo() 来检索当前连接信息。
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
应用可使用 requestDeviceInfo() 来检索当前连接信息。

创建 WLAN P2P 应用

创建 WLAN P2P 应用涉及为应用创建并注册广播接收器、发现对等设备,连接到对等设备,以及将数据传输到对等设备。以下部分将介绍如何完成此操作。

初始设置

在使用 WLAN P2P API 之前,您必须确保您的应用可以访问硬件,并且设备支持 WLAN P2P API 协议。如果设备支持 WLAN P2P,您可以获得 WifiP2pManager 的实例,创建并注册广播接收器,然后开始使用 WLAN P2P API。

请求在设备上使用 WLAN 硬件的权限,同时声明您的应用在 Android 清单中具有正确的最低 SDK 版本:

<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.ACCESS_FINE_LOCATION" />
<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" />

除上述权限以外,您还需要启用位置信息模式才能使用下列 API:

discoverPeers
discoverServices
requestPeers
检查 WLAN P2P 是否开启并受支持。您可以在广播接收器收到 WIFI_P2P_STATE_CHANGED_ACTION Intent 时,在接收器中检查此项。向您的 Activity 通知 WLAN P2P 的状态,并作出相应回应:

Kotlin

override fun onReceive(context: Context, intent: Intent) {
...
val action: String = intent.action
when (action) {
    WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
        val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1)
        when (state) {
            WifiP2pManager.WIFI_P2P_STATE_ENABLED -> {
                // Wifi P2P is enabled
            }
            else -> {
                // Wi-Fi P2P is not enabled
            }
        }
    }
}
...
}

Java

@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 P2P is enabled
    } else {
        // Wi-Fi P2P is not enabled
    }
}
...
}

在 Activity 的 onCreate() 方法中,获取 WifiP2pManager 的实例,并通过调用 initialize(),在 WLAN P2P 框架中注册您的应用。此方法会返回 WifiP2pManager.Channel,用于将您的应用连接到 WLAN P2P 框架。此外,您还应该通过 WifiP2pManager 和 WifiP2pManager.Channel 对象以及对 Activity 的引用,创建广播接收器实例。这样广播接收器便可通知 Activity 感兴趣的事件并进行相应更新。此外,您还可以操纵设备的 WLAN 状态(如有必要):

Kotlin

val manager: WifiP2pManager? by lazy(LazyThreadSafetyMode.NONE) {
    getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager?
}

var mChannel: WifiP2pManager.Channel? = null
var receiver: BroadcastReceiver? = null

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    mChannel = manager?.initialize(this, mainLooper, null)
    mChannel?.also { channel ->
        receiver = WiFiDirectBroadcastReceiver(manager, channel, this)
    }

}

Java

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

创建 Intent 过滤器,然后添加与广播接收器检查内容相同的 Intent:

Kotlin

val intentFilter = IntentFilter().apply {
    addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)
    addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
    addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
    addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION)
}

Java

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

在 Activity 的 onResume() 方法中注册广播接收器,然后在 Activity 的onPause() 方法中取消注册该接收器:

Kotlin

/* register the broadcast receiver with the intent values to be matched */
override fun onResume() {
    super.onResume()
    mReceiver?.also { receiver ->
        registerReceiver(receiver, intentFilter)
    }
}

/* unregister the broadcast receiver */
override fun onPause() {
    super.onPause()
    mReceiver?.also { receiver ->
        unregisterReceiver(receiver)
    }
}

Java

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

获取 WifiP2pManager.Channel 并设置广播接收器后,应用便可调用 WLAN P2P 方法并收到 WLAN P2P Intent。

现在,您可以实现应用,然后通过调用 WifiP2pManager 中的方法,使用 WLAN P2P 功能。下一部分介绍如何执行常见操作,例如发现和连接到对等设备。

发现对等设备

如要发现可连接的对等设备,请调用 discoverPeers(),以检测范围内的可用对等设备。对此功能的调用为异步操作,如果您已创建 WifiP2pManager.ActionListener,则系统会通过 onSuccess() 和 onFailure() 告知应用成功与否。onSuccess() 方法仅会通知您发现进程已成功,但不会提供有关其发现的实际对等设备(如有)的任何信息:

Kotlin

manager?.discoverPeers(channel, object : WifiP2pManager.ActionListener {

    override fun onSuccess() {
        ...
    }

    override fun onFailure(reasonCode: Int) {
        ...
    }
})

Java

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

    @Override
    public void onFailure(int reasonCode) {
        ...
    }
});

如果发现进程成功并检测到对等设备,则系统会广播 WIFI_P2P_PEERS_CHANGED_ACTION Intent,您可以在广播接收器中侦听该 Intent,以获取对等设备列表。当应用接收到 WIFI_P2P_PEERS_CHANGED_ACTION Intent 时,您可以通过 requestPeers() 请求已发现对等设备的列表。以下代码展示如何完成此项设置:

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    val action: String = intent.action
    when (action) {
        ...
        WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
            manager?.requestPeers(channel) { peers: WifiP2pDeviceList? ->
                // Handle peers list
            }
        }
        ...
    }
}

Java

PeerListListener myPeerListListener;
...
if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {

    // request available peers from the wifi p2p manager. This is an
    // asynchronous call and the calling activity is notified with a
    // callback on PeerListListener.onPeersAvailable()
    if (manager != null) {
        manager.requestPeers(channel, myPeerListListener);
    }
}

requestPeers() 方法也为异步操作,并可在对等设备列表可用时通过 onPeersAvailable()(定义见 WifiP2pManager.PeerListListener 接口)通知您的 Activity。onPeersAvailable() 方法为您提供 WifiP2pDeviceList,您可对其进行迭代以查找希望连接的对等设备。

连接到对等设备

获取可能对等设备的列表,且已确定您要连接的设备后,调用connect() 方法即可连接到相应设备。调用此方法需要使用 WifiP2pConfig 对象,其中包含要连接的设备的信息。您可以通过 WifiP2pManager.ActionListener 获知连接是否成功。以下代码展示如何创建与所需设备的连接:

Kotlin

val device: WifiP2pDevice = ...
val config = WifiP2pConfig()
config.deviceAddress = device.deviceAddress
mChannel?.also { channel ->
    manager?.connect(channel, config, object : WifiP2pManager.ActionListener {

        override fun onSuccess() {
            //success logic
        }

        override fun onFailure(reason: Int) {
            //failure logic
        }
}
})

Java

//obtain a peer from the WifiP2pDeviceList
WifiP2pDevice device;
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = device.deviceAddress;
manager.connect(channel, config, new ActionListener() {

    @Override
    public void onSuccess() {
        //success logic
    }

    @Override
    public void onFailure(int reason) {
        //failure logic
    }
});

传输数据

建立连接后,您可以通过套接字在设备之间传输数据。数据传输的基本步骤如下:

创建 ServerSocket。此套接字会在指定端口等待来自客户端的连接,然后加以屏蔽直到连接发生,因此请在后台线程中也执行此操作。

创建客户端 Socket。客户端使用 IP 地址和服务器套接字端口连接到服务器设备。

将数据从客户端发送到服务器。客户端套接字成功连接到服务器套接字后,您可以通过字节流将数据从客户端发送到服务器。

服务器套接字等待客户端连接(通过 accept() 方法)。在客户端连接前,此调用会屏蔽连接,所以这是另一个线程。发生连接时,服务器设备可接收到客户端数据。对这些数据执行任何操作,例如将其保存到文件中,或向用户显示这些数据。

以下示例(修改自 WLAN P2P 演示示例)展示如何创建此客户端-服务器套接字通信,以及如何通过服务将 JPEG 图像从客户端传输到服务器。如需完整工作示例,请编译并运行 WLAN P2P 演示示例。

Kotlin

class FileServerAsyncTask(
        private val context: Context,
        private var statusText: TextView
) : AsyncTask<Void, Void, String?>() {

    override fun doInBackground(vararg params: Void): String? {
        /**
         * Create a server socket.
         */
        val serverSocket = ServerSocket(8888)
        return serverSocket.use {
            /**
             * Wait for client connections. This call blocks until a
             * connection is accepted from a client.
             */
            val client = serverSocket.accept()
            /**
             * If this code is reached, a client has connected and transferred data
             * Save the input stream from the client as a JPEG file
             */
            val f = File(Environment.getExternalStorageDirectory().absolutePath +
                    "/${context.packageName}/wifip2pshared-${System.currentTimeMillis()}.jpg")
            val dirs = File(f.parent)

            dirs.takeIf { it.doesNotExist() }?.apply {
                mkdirs()
            }
            f.createNewFile()
            val inputstream = client.getInputStream()
            copyFile(inputstream, FileOutputStream(f))
            serverSocket.close()
            f.absolutePath
        }
    }

    private fun File.doesNotExist(): Boolean = !exists()

    /**
     * Start activity that can handle the JPEG image
     */
    override fun onPostExecute(result: String?) {
        result?.run {
            statusText.text = "File copied - $result"
            val intent = Intent(android.content.Intent.ACTION_VIEW).apply {
                setDataAndType(Uri.parse("file://$result"), "image/*")
            }
            context.startActivity(intent)
        }
    }
}

Java

public static class FileServerAsyncTask extends AsyncTask {

    private Context context;
    private TextView statusText;

    public FileServerAsyncTask(Context context, View statusText) {
        this.context = context;
        this.statusText = (TextView) statusText;
    }

    @Override
    protected String doInBackground(Void... params) {
        try {

            /**
             * Create a server socket and wait for client connections. This
             * call blocks until a connection is accepted from a client
             */
            ServerSocket serverSocket = new ServerSocket(8888);
            Socket client = serverSocket.accept();

            /**
             * If this code is reached, a client has connected and transferred data
             * Save the input stream from the client as a JPEG file
             */
            final File f = new File(Environment.getExternalStorageDirectory() + "/"
                    + context.getPackageName() + "/wifip2pshared-" + System.currentTimeMillis()
                    + ".jpg");

            File dirs = new File(f.getParent());
            if (!dirs.exists())
                dirs.mkdirs();
            f.createNewFile();
            InputStream inputstream = client.getInputStream();
            copyFile(inputstream, new FileOutputStream(f));
            serverSocket.close();
            return f.getAbsolutePath();
        } catch (IOException e) {
            Log.e(WiFiDirectActivity.TAG, e.getMessage());
            return null;
        }
    }

    /**
     * Start activity that can handle the JPEG image
     */
    @Override
    protected void onPostExecute(String result) {
        if (result != null) {
            statusText.setText("File copied - " + result);
            Intent intent = new Intent();
            intent.setAction(android.content.Intent.ACTION_VIEW);
            intent.setDataAndType(Uri.parse("file://" + result), "image/*");
            context.startActivity(intent);
        }
    }
}

在客户端上,通过客户端套接字连接到服务器套接字,然后传输数据。本示例传输的是客户端设备文件系统中的 JPEG 文件。

Kotlin

val context = applicationContext
val host: String
val port: Int
val len: Int
val socket = Socket()
val buf = ByteArray(1024)
...
try {
    /**
     * Create a client socket with the host,
     * port, and timeout information.
     */
    socket.bind(null)
    socket.connect((InetSocketAddress(host, port)), 500)

    /**
     * Create a byte stream from a JPEG file and pipe it to the output stream
     * of the socket. This data is retrieved by the server device.
     */
    val outputStream = socket.getOutputStream()
    val cr = context.contentResolver
    val inputStream: InputStream = cr.openInputStream(Uri.parse("path/to/picture.jpg"))
    while (inputStream.read(buf).also { len = it } != -1) {
        outputStream.write(buf, 0, len)
    }
    outputStream.close()
    inputStream.close()
} catch (e: FileNotFoundException) {
    //catch logic
} catch (e: IOException) {
    //catch logic
} finally {
    /**
     * Clean up any open sockets when done
     * transferring or if an exception occurred.
     */
    socket.takeIf { it.isConnected }?.apply {
        close()
    }
}

Java

Context context = this.getApplicationContext();
String host;
int port;
int len;
Socket socket = new Socket();
byte buf[]  = new byte[1024];
...
try {
    /**
     * Create a client socket with the host,
     * port, and timeout information.
     */
    socket.bind(null);
    socket.connect((new InetSocketAddress(host, port)), 500);

    /**
     * Create a byte stream from a JPEG file and pipe it to the output stream
     * of the socket. This data is retrieved by the server device.
     */
    OutputStream outputStream = socket.getOutputStream();
    ContentResolver cr = context.getContentResolver();
    InputStream inputStream = null;
    inputStream = cr.openInputStream(Uri.parse("path/to/picture.jpg"));
    while ((len = inputStream.read(buf)) != -1) {
        outputStream.write(buf, 0, len);
    }
    outputStream.close();
    inputStream.close();
} catch (FileNotFoundException e) {
    //catch logic
} catch (IOException e) {
    //catch logic
}

/**
 * Clean up any open sockets when done
 * transferring or if an exception occurred.
 */
finally {
    if (socket != null) {
        if (socket.isConnected()) {
            try {
                socket.close();
            } catch (IOException e) {
                //catch logic
            }
        }
    }
}

本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。

最后更新时间 (UTC):2023-11-02。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五一编程

程序之路有我与你同行

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值