新聞中心
最近的項目需要實現(xiàn)一個 Android 手機之間無網(wǎng)絡傳輸文件的功能,就發(fā)現(xiàn)了 Wifi P2P(Wifi點對點)這么一個功能,最后也實現(xiàn)了通過 Wifi 隔空傳輸文件的功能,這里我也來整理下代碼,分享給大家。

10年積累的網(wǎng)站建設、網(wǎng)站制作經(jīng)驗,可以快速應對客戶對網(wǎng)站的新想法和需求。提供各種問題對應的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡服務。我雖然不認識你,你也不認識我。但先網(wǎng)站設計后付款的網(wǎng)站建設流程,更有海珠免費網(wǎng)站建設讓你可以放心的選擇與我們合作。
Wifi P2P 是在 Android 4.0 以及更高版本系統(tǒng)中加入的功能,通過 Wifi P2P 可以在不連接網(wǎng)絡的情況下,直接與配對的設備進行數(shù)據(jù)交換。相對于藍牙,Wifi P2P 的搜索速度和傳輸速度更快,傳輸距離更遠
實現(xiàn)的效果如下所示:
一般而言,開發(fā)步驟分為以下幾點:
- 在 AndroidManifest 中聲明相關權限(網(wǎng)絡和文件讀寫權限)
- 獲取 WifiP2pManager ,注冊相關廣播監(jiān)聽Wifi直連的狀態(tài)變化
- 指定某一臺設備為服務器(用來接收文件),創(chuàng)建群組并作為群主存在,在指定端口監(jiān)聽客戶端的連接請求,等待客戶端發(fā)起連接請求以及文件傳輸請求
- 客戶端(用來發(fā)送文件)主動搜索附近的設備,加入到服務器創(chuàng)建的群組,獲取服務器的IP地址,向其發(fā)起文件傳輸請求
- 校驗文件完整性
一、聲明權限
Wifi P2P 技術并不會訪問網(wǎng)絡,但由于會使用到 Java socket,所以需要申請網(wǎng)絡權限。此外,由于是要實現(xiàn)文件互傳,所以也需要申請SD卡讀寫權限。
二、注冊廣播
與 Wifi P2P 相關的廣播有以下幾個:
- WIFI_P2P_STATE_CHANGED_ACTION( 用于指示 Wifi P2P 是否可用 )
- WIFI_P2P_PEERS_CHANGED_ACTION( 對等節(jié)點列表發(fā)生了變化 )
- WIFI_P2P_CONNECTION_CHANGED_ACTION( Wifi P2P 的連接狀態(tài)發(fā)生了改變 )
- WIFI_P2P_THIS_DEVICE_CHANGED_ACTION( 本設備的設備信息發(fā)生了變化 )
當接收到這幾個廣播時,我們都需要到 WifiP2pManager (對等網(wǎng)絡管理器)來進行相應的信息請求,此外還需要用到 Channel 對象作為請求參數(shù)
- mWifiP2pManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
- mChannel = mWifiP2pManager.initialize(this, getMainLooper(), this);
當收到 WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION 廣播時,可以判斷當前 Wifi P2P是否可用
- int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
- mDirectActionListener.wifiP2pEnabled(true);
- } else {
- mDirectActionListener.wifiP2pEnabled(false);
- }
當收到 WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION 廣播時,意味設備周圍的可用設備列表發(fā)生了變化,可以通過 requestPeers 方法得到可用的設備列表,之后就可以選擇當中的某一個設備進行連接操作
- mWifiP2pManager.requestPeers(mChannel, new WifiP2pManager.PeerListListener() { @Override
- public void onPeersAvailable(WifiP2pDeviceList peers) {
- mDirectActionListener.onPeersAvailable(peers.getDeviceList());
- }
- });
當收到 WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION 廣播時,意味著 Wifi P2P 的連接狀態(tài)發(fā)生了變化,可能是連接到了某設備,或者是與某設備斷開了連接
- NetworkInfo networkInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);if (networkInfo.isConnected()) {
- mWifiP2pManager.requestConnectionInfo(mChannel, new WifiP2pManager.ConnectionInfoListener() { @Override
- public void onConnectionInfoAvailable(WifiP2pInfo info) {
- mDirectActionListener.onConnectionInfoAvailable(info);
- }
- });
- Log.e(TAG, "已連接p2p設備");
- } else {
- mDirectActionListener.onDisconnection();
- Log.e(TAG, "與p2p設備已斷開連接");
- }
如果是與某設備連接上了,則可以通過 requestConnectionInfo 方法獲取到連接信息
當收到 WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION 廣播時,則可以獲取到本設備變化后的設備信息
- (WifiP2pDevice) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE)
可以看出 Wifi P2P 的接口高度異步化,到現(xiàn)在已經(jīng)用到了三個系統(tǒng)的回調函數(shù),一個用于 WifiP2pManager 的初始化,兩個用于在廣播中異步請求數(shù)據(jù),為了簡化操作,此處統(tǒng)一使用一個自定義的回調函數(shù),方法含義與系統(tǒng)的回調函數(shù)一致
- public interface DirectActionListener extends WifiP2pManager.ChannelListener {
- void wifiP2pEnabled(boolean enabled);
- void onConnectionInfoAvailable(WifiP2pInfo wifiP2pInfo);
- void onDisconnection();
- void onSelfDeviceAvailable(WifiP2pDevice wifiP2pDevice);
- void onPeersAvailable(Collection
wifiP2pDeviceList); - }
所以,整個廣播接收器使用到的所有代碼是:
- /**
- * 作者:chenZY
- * 時間:2018/2/9 17:53
- * 描述:
- */
- public class DirectBroadcastReceiver extends BroadcastReceiver {
- private static final String TAG = "DirectBroadcastReceiver";
- private WifiP2pManager mWifiP2pManager;
- private WifiP2pManager.Channel mChannel;
- private DirectActionListener mDirectActionListener;
- public DirectBroadcastReceiver(WifiP2pManager wifiP2pManager, WifiP2pManager.Channel channel, DirectActionListener directActionListener) {
- mWifiP2pManager = wifiP2pManager;
- mChannel = channel;
- mDirectActionListener = directActionListener;
- }
- public static IntentFilter getIntentFilter() {
- IntentFilter 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);
- return intentFilter;
- }
- @Override
- public void onReceive(Context context, Intent intent) {
- Log.e(TAG, "接收到廣播: " + intent.getAction());
- if (!TextUtils.isEmpty(intent.getAction())) {
- switch (intent.getAction()) {
- // 用于指示 Wifi P2P 是否可用
- case WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION: {
- int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
- if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
- mDirectActionListener.wifiP2pEnabled(true);
- } else {
- mDirectActionListener.wifiP2pEnabled(false);
- List
wifiP2pDeviceList = new ArrayList<>(); - mDirectActionListener.onPeersAvailable(wifiP2pDeviceList);
- }
- break;
- }
- // 對等節(jié)點列表發(fā)生了變化
- case WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION: {
- mWifiP2pManager.requestPeers(mChannel, new WifiP2pManager.PeerListListener() {
- @Override
- public void onPeersAvailable(WifiP2pDeviceList peers) {
- mDirectActionListener.onPeersAvailable(peers.getDeviceList());
- }
- });
- break;
- }
- // Wifi P2P 的連接狀態(tài)發(fā)生了改變
- case WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION: {
- NetworkInfo networkInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
- if (networkInfo.isConnected()) {
- mWifiP2pManager.requestConnectionInfo(mChannel, new WifiP2pManager.ConnectionInfoListener() {
- @Override
- public void onConnectionInfoAvailable(WifiP2pInfo info) {
- mDirectActionListener.onConnectionInfoAvailable(info);
- }
- });
- Log.e(TAG, "已連接p2p設備");
- } else {
- mDirectActionListener.onDisconnection();
- Log.e(TAG, "與p2p設備已斷開連接");
- }
- break;
- }
- //本設備的設備信息發(fā)生了變化
- case WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION: {
- mDirectActionListener.onSelfDeviceAvailable((WifiP2pDevice) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE));
- break;
- }
- }
- }
- }
- }
三、服務器端創(chuàng)建群組
假設當設備A搜索到了設備B,并與設備B連接到了一起,此時系統(tǒng)會自動創(chuàng)建一個群組(Group)并隨機指定一臺設備為群主(GroupOwner)。此時,對于兩臺設備來說,群主的IP地址是可知的(系統(tǒng)回調函數(shù)中有提供),但客戶端的IP地址需要再來通過其他方法來主動獲取。例如,可以在設備連接成功后,客戶端主動發(fā)起對服務器端的Socket連接請求,服務器端在指定端口監(jiān)聽客戶端的連接請求,當連接成功后,服務器端就可以獲取到客戶端的IP地址了
此處為了簡化操作,直接指定某臺設備作為服務器端(群主),即直接指定某臺設備用來接收文件
因此,服務器端要主動創(chuàng)建群組,并等待客戶端的連接
- wifiP2pManager.createGroup(channel, new WifiP2pManager.ActionListener() {
- @Override
- public void onSuccess() {
- Log.e(TAG, "createGroup onSuccess");
- dismissLoadingDialog();
- showToast("onSuccess");
- }
- @Override
- public void onFailure(int reason) {
- Log.e(TAG, "createGroup onFailure: " + reason);
- dismissLoadingDialog();
- showToast("onFailure");
- }
- });
此處,使用 IntentService 在后臺監(jiān)聽客戶端的Socket連接請求,并通過輸入輸出流來傳輸文件。此處的代碼比較簡單,就只是在指定端口一直堵塞監(jiān)聽客戶端的連接請求,獲取待傳輸?shù)奈募畔⒛P?FileTransfer ,之后就進行實際的數(shù)據(jù)傳輸
- @Override
- protected void onHandleIntent(Intent intent) {
- clean();
- File file = null;
- try {
- serverSocket = new ServerSocket();
- serverSocket.setReuseAddress(true);
- serverSocket.bind(new InetSocketAddress(PORT));
- Socket client = serverSocket.accept();
- Log.e(TAG, "客戶端IP地址 : " + client.getInetAddress().getHostAddress());
- inputStream = client.getInputStream();
- objectInputStream = new ObjectInputStream(inputStream);
- FileTransfer fileTransfer = (FileTransfer) objectInputStream.readObject();
- Log.e(TAG, "待接收的文件: " + fileTransfer);
- String name = new File(fileTransfer.getFilePath()).getName();
- //將文件存儲至指定位置
- file = new File(Environment.getExternalStorageDirectory() + "/" + name);
- fileOutputStream = new FileOutputStream(file);
- byte buf[] = new byte[512];
- int len;
- long total = 0;
- int progress;
- while ((len = inputStream.read(buf)) != -1) {
- fileOutputStream.write(buf, 0, len);
- total += len;
- progress = (int) ((total * 100) / fileTransfer.getFileLength());
- Log.e(TAG, "文件接收進度: " + progress);
- if (progressChangListener != null) {
- progressChangListener.onProgressChanged(fileTransfer, progress);
- }
- }
- serverSocket.close();
- inputStream.close();
- objectInputStream.close();
- fileOutputStream.close();
- serverSocket = null;
- inputStream = null;
- objectInputStream = null;
- fileOutputStream = null;
- Log.e(TAG, "文件接收成功,文件的MD5碼是:" + Md5Util.getMd5(file));
- } catch (Exception e) {
- Log.e(TAG, "文件接收 Exception: " + e.getMessage());
- } finally {
- clean();
- if (progressChangListener != null) {
- progressChangListener.onTransferFinished(file);
- }
- //再次啟動服務,等待客戶端下次連接
- startService(new Intent(this, WifiServerService.class));
- }
- }
因為客戶端可能會多次發(fā)起連接請求,所以當此處文件傳輸完成后(不管成功或失?。?,都需要重新 startService ,讓服務再次堵塞等待客戶端的連接請求
FileTransfer 包含三個字段,MD5碼值用于校驗文件的完整性,fileLength 是為了用于計算文件的傳輸進度
- public class FileTransfer implements Serializable {
- //文件路徑
- private String filePath;
- //文件大小
- private long fileLength;
- //MD5碼
- private String md5;
- ···
- }
為了將文件傳輸進度發(fā)布到外部界面,所以除了需要啟動Service外,界面還需要綁定Service,此處就需要用到一個更新文件傳輸狀態(tài)的接口
- public interface OnProgressChangListener {
- //當傳輸進度發(fā)生變化時
- void onProgressChanged(FileTransfer fileTransfer, int progress);
- //當傳輸結束時
- void onTransferFinished(File file);
- }
因此,需要將 progressChangListener 作為參數(shù)傳給 WifiServerService ,并在進度變化時更新進度對話框
- private WifiServerService.OnProgressChangListener progressChangListener = new WifiServerService.OnProgressChangListener() {
- @Override
- public void onProgressChanged(final FileTransfer fileTransfer, final int progress) {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- progressDialog.setMessage("文件名: " + new File(fileTransfer.getFilePath()).getName());
- progressDialog.setProgress(progress);
- progressDialog.show();
- }
- });
- }
- @Override
- public void onTransferFinished(final File file) {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- progressDialog.cancel();
- if (file != null && file.exists()) {
- openFile(file.getPath());
- }
- }
- });
- }
- };
四、客戶端加入群組并發(fā)起文件傳輸請求
文件發(fā)送界面 SendFileActivity 需要實現(xiàn) DirectActionListener 接口
首先,需要先注冊P2P廣播,以便獲取周邊設備信息以及連接狀態(tài)
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_send_file);
- initView();
- mWifiP2pManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
- mChannel = mWifiP2pManager.initialize(this, getMainLooper(), this);
- broadcastReceiver = new DirectBroadcastReceiver(mWifiP2pManager, mChannel, this);
- registerReceiver(broadcastReceiver, DirectBroadcastReceiver.getIntentFilter());
- }
通過 discoverPeers 方法搜索周邊設備,回調函數(shù)用于通知方法是否調用成功
- mWifiP2pManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() { @Override
- public void onSuccess() {
- showToast("Success");
- }
- @Override
- public void onFailure(int reasonCode) {
- showToast("Failure");
- loadingDialog.cancel();
- }
- });
當搜索結束后,系統(tǒng)就會觸發(fā) WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION 廣播,此時就可以調用 requestPeers 方法獲取設備列表信息,此處用 RecyclerView 展示列表,在 onPeersAvailable 方法刷新列表
- mWifiP2pManager.requestPeers(mChannel, new WifiP2pManager.PeerListListener() {
- @Override
- public void onPeersAvailable(WifiP2pDeviceList peers) {
- mDirectActionListener.onPeersAvailable(peers.getDeviceList());
- }
- });
- @Override
- public void onPeersAvailable(Collection
wifiP2pDeviceList) { - Log.e(TAG, "onPeersAvailable :" + wifiP2pDeviceList.size());
- this.wifiP2pDeviceList.clear();
- this.wifiP2pDeviceList.addAll(wifiP2pDeviceList);
- deviceAdapter.notifyDataSetChanged();
- loadingDialog.cancel();
- }
之后,通過點擊事件選中群主(服務器端)設備,通過 connect 方法請求與之進行連接
- private void connect() {
- WifiP2pConfig config = new WifiP2pConfig();
- if (config.deviceAddress != null && mWifiP2pDevice != null) {
- config.deviceAddress = mWifiP2pDevice.deviceAddress;
- config.wps.setup = WpsInfo.PBC;
- showLoadingDialog("正在連接 " + mWifiP2pDevice.deviceName);
- mWifiP2pManager.connect(mChannel, config, new WifiP2pManager.ActionListener() {
- @Override
- public void onSuccess() {
- Log.e(TAG, "connect onSuccess");
- }
- @Override
- public void onFailure(int reason) {
- showToast("連接失敗 " + reason);
- dismissLoadingDialog();
- }
- });
- }
- }
此處依然無法通過函數(shù)函數(shù)來判斷連接結果,需要依靠系統(tǒng)發(fā)出的 WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION 方法來獲取到連接結果,在此處可以通過 requestConnectionInfo 獲取到組連接信息,信息最后通過 onConnectionInfoAvailable 方法傳遞出來,在此可以判斷當前設備是否為群主,獲取群組IP地址
- @Override
- public void onConnectionInfoAvailable(WifiP2pInfo wifiP2pInfo) {
- dismissLoadingDialog();
- wifiP2pDeviceList.clear();
- deviceAdapter.notifyDataSetChanged();
- btn_disconnect.setEnabled(true);
- btn_chooseFile.setEnabled(true);
- Log.e(TAG, "onConnectionInfoAvailable");
- Log.e(TAG, "onConnectionInfoAvailable groupFormed: " + wifiP2pInfo.groupFormed);
- Log.e(TAG, "onConnectionInfoAvailable isGroupOwner: " + wifiP2pInfo.isGroupOwner);
- Log.e(TAG, "onConnectionInfoAvailable getHostAddress: " + wifiP2pInfo.groupOwnerAddress.getHostAddress());
- StringBuilder stringBuilder = new StringBuilder();
- if (mWifiP2pDevice != null) {
- stringBuilder.append("連接的設備名:");
- stringBuilder.append(mWifiP2pDevice.deviceName);
- stringBuilder.append("\n");
- stringBuilder.append("連接的設備的地址:");
- stringBuilder.append(mWifiP2pDevice.deviceAddress);
- }
- stringBuilder.append("\n");
- stringBuilder.append("是否群主:");
- stringBuilder.append(wifiP2pInfo.isGroupOwner ? "是群主" : "非群主");
- stringBuilder.append("\n");
- stringBuilder.append("群主IP地址:");
- stringBuilder.append(wifiP2pInfo.groupOwnerAddress.getHostAddress());
- tv_status.setText(stringBuilder);
- if (wifiP2pInfo.groupFormed && !wifiP2pInfo.isGroupOwner) {
- this.wifiP2pInfo = wifiP2pInfo;
- }
- }
至此服務器端和客戶端已經(jīng)通過 Wifi P2P 連接在了一起,客戶端也獲取到了服務器端的IP地址,在選取好待發(fā)送的文件后就可以主動發(fā)起對服務器端的連接請求了
發(fā)起選取文件的方法
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.setType("*/*");
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- startActivityForResult(intent, 1);
獲取選取的文件的實際路徑
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == 1) {
- if (resultCode == RESULT_OK) {
- Uri uri = data.getData();
- if (uri != null) {
- String path = getPath(this, uri);
- if (path != null) {
- File file = new File(path);
- if (file.exists() && wifiP2pInfo != null) {
- FileTransfer fileTransfer = new FileTransfer(file.getPath(), file.length());
- Log.e(TAG, "待發(fā)送的文件:" + fileTransfer);
- new WifiClientTask(this, fileTransfer).execute(wifiP2pInfo.groupOwnerAddress.getHostAddress());
- }
- }
- }
- }
- }
- }
- private String getPath(Context context, Uri uri) {
- if ("content".equalsIgnoreCase(uri.getScheme())) {
- Cursor cursor = context.getContentResolver().query(uri, new String[]{"_data"}, null, null, null);
- if (cursor != null) {
- if (cursor.moveToFirst()) {
- String data = cursor.getString(cursor.getColumnIndex("_data"));
- cursor.close();
- return data;
- }
- }
- } else if ("file".equalsIgnoreCase(uri.getScheme())) {
- return uri.getPath();
- }
- return null;
- }
文件的發(fā)送操作放到 AsyncTask 中處理,將服務器端的IP地址作為參數(shù)傳進來,在正式發(fā)送文件前,先發(fā)送包含文件信息(文件名,文件大小,文件MD5碼)的信息模型 FileTransfer ,并在發(fā)送文件的過程中同時更新進度
- /**
- * 作者:葉應是葉
- * 時間:2018/2/15 8:51
- * 描述:客戶端發(fā)送文件
- */
- public class WifiClientTask extends AsyncTask
{ - private ProgressDialog progressDialog;
- private FileTransfer fileTransfer;  
新聞名稱:Android如何實現(xiàn)無網(wǎng)絡傳輸文件
本文鏈接:http://www.dlmjj.cn/article/dpdoedh.html


咨詢
建站咨詢
