Android學習筆記第五篇–網絡連接與雲服務
第一章、無線連接設備
除了能夠在雲端通訊,Android的無線API也允許在同一局域網內的設備通訊,**甚至沒有連接網絡,而是物理具體相近,也可以相互通訊。**Network Service Discovery 簡稱NSD可以允許應用相互通訊發現附近設備。
本節主要介紹Android應用發現與連接其他設備的API。主要介紹NSD的API和點對點無線(the Wi-Fi Peer-to-Peer)API。
1、使用網絡服務發現(NSD)
添加NSD服務到App中,可以使用戶辨識在局域網內支持app請求的設備。有助於更好的實現文件共享、聯機遊戲等服務需求。
註冊NSD服務
Note:註冊NSD服務爲非必選項,若是不關注本地網絡的廣播,則可以不用註冊。
在局域網內註冊自身服務首先要創建
NsdServiceInfo
對象。public void registerService(int port){ //創建並初始化NSD對象 NsdServiceInfo serviceInfo = new NsdServiceInfo(); //服務名稱要保證唯一性 serviceInfo.serServiceName("NsdChat"); //指定協議和傳輸層,如指定打印服務"_ipp._tcp" serviceInfo.setServiceType("_http._tcp."); serviceInfo.setPort(port); ..... }
如上創建了一個NSD服務,並設置了名稱、服務類型。其中服務類型制定的是應用使用的協議和傳輸層。語法是
_<protocol>._<transportlayer>
。Note:互聯網編號分配機構(International Assigned Numbers Authority)提供用於服務發現協議,如NSD和Bonjour等。
服務端口號應避免硬代碼,以便於可以動態更改端口號,並更新通知。
public void initializeServerSocket(){ //初始化一個server socket,指定下面的端口 mServiceSocket = new ServerSocket(0); //存儲選擇的端口號 mLocalPort = mServerSocket.getLocalPort(); ...... }
至此已經創建了
NsdServiceInfo
對象,接着要實現RegistrationListener
接口,實現註冊功能。public void initializeRegistrationListener(){ mRegistrationListener = new NsdManager.RegistrationListener(){ @Override public void onServiceRegistered(NsdServiceInfo nsdServiceInfo){ //需要更新已經保存的註冊服務名稱,因爲它需要唯一性,若是命名衝突,Android會自動解決衝突,此處就需要更新獲取。 mServiceName = nsdServiceInfo.getServiceName(); } @Override public void onRegistrationFailed(NsdServiceInfo serviceInfo,int errorCode){ //註冊失敗時候,在此處可以記錄日誌 } @Override public void onServiceUnregistered(NsdServiceInfo arg0){ //註銷服務,只有通過NsdManager來註銷纔會調用這裏。 } @Override public void onUnregistrationFailed(NsdService serviceInfo,int errorCode){ //註銷失敗,記錄日誌 } }; }
因爲
registerService()
方法是異步的,在註冊服務之後的操作,需要在onServiceRegistered()
方法中進行。public void registerService(int port){ NsdServiceInfo serviceInfo = new NsdServiceInfo(); serviceInfo.setServiceName("NsdChat"); serviceInfo.setServiceType("_http._tcp."); serviceInfo.setPort(Port); mNsdManager = Context.getSystemService(Context.NSD_SERVICE); mNsdManager.registerService(serviceInfo,NsdManager.PROTOCOL_DNS_SD,mRegistrationListener); }
發現網絡中的服務
發現網絡服務需要兩步:
- 註冊網絡監聽器
- 調用
discoverServices()
異步API
1、創建
NsdManager.DiscoveryListener
接口的實現類。public void initializeDiscoveryListener(){ //實例化網絡發現監聽器 mDiscoverListener = new NsdManager.DiscoveryListener(){ //發現服務時候調用該方法 @Override public void onDiscoveryStarted(String regType){ Log.d(TAG,"Service discovery started"); } @Override public void onServiceFound(NsdServiceInfo service) { // A service was found! Do something with it. Log.d(TAG, "Service discovery success" + service); if (!service.getServiceType().equals(SERVICE_TYPE)) { // Service type is the string containing the protocol and // transport layer for this service. Log.d(TAG, "Unknown Service Type: " + service.getServiceType()); } else if (service.getServiceName().equals(mServiceName)) { // The name of the service tells the user what they'd be // connecting to. It could be "Bob's Chat App". Log.d(TAG, "Same machine: " + mServiceName); } else if (service.getServiceName().contains("NsdChat")){ mNsdManager.resolveService(service, mResolveListener); } } @Override public void onServiceLost(NsdServiceInfo service) { // When the network service is no longer available. // Internal bookkeeping code goes here. Log.e(TAG, "service lost" + service); } @Override public void onDiscoveryStopped(String serviceType) { Log.i(TAG, "Discovery stopped: " + serviceType); } @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { Log.e(TAG, "Discovery failed: Error code:" + errorCode); mNsdManager.stopServiceDiscovery(this); } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { Log.e(TAG, "Discovery failed: Error code:" + errorCode); mNsdManager.stopServiceDiscovery(this); } }; } }
NSD API
通過使用該接口中的方法,可以對網絡服務狀態進行監控。設置好監聽器後,調用discoverService()
函數:mNsdManager.discoveryService(SERVICE_TYPE,NsdManager.PROTOCOL_DNS_SD,mDiscoveryListener);
連接到網絡上的服務
發現網絡上的可接入服務時,首先調用resolveService()方法,來確定服務連接信息。實現
NsdManage.ResolveListener
對象並將其傳入resolveService()
方法,並使用該對象獲得NsdSerServiceInfo
。public void initializeResolveListener(){ mResolveListener = new NsdManager.ResolveListener(){ @Override public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { // Called when the resolve fails. Use the error code to debug. Log.e(TAG, "Resolve failed" + errorCode); } @Override public void onServiceResolved(NsdServiceInfo serviceInfo) { Log.e(TAG, "Resolve Succeeded. " + serviceInfo); if (serviceInfo.getServiceName().equals(mServiceName)) { Log.d(TAG, "Same IP."); return; } mService = serviceInfo; int port = mService.getPort(); InetAddress host = mService.getHost(); } }; }
至此完成服務接入,即可實現本地與之通訊。
程序退出註銷服務
使用NSD服務是比較消耗資源的,而且重複鏈接會導致問題,所以需要在app生命週期內的合適階段開啓、關閉服務。
//Activity @Override protected void onPause() { if (mNsdHelper != null) { mNsdHelper.tearDown(); } super.onPause(); } @Override protected void onResume() { super.onResume(); if (mNsdHelper != null) { mNsdHelper.registerService(mConnection.getLocalPort()); mNsdHelper.discoverServices(); } } @Override protected void onDestroy() { mNsdHelper.tearDown(); mConnection.tearDown(); super.onDestroy(); } // NsdHelper's tearDown method public void tearDown() { mNsdManager.unregisterService(mRegistrationListener); mNsdManager.stopServiceDiscovery(mDiscoveryListener); }
2、使用WiFi建立P2P連接
WiFi點對點(P2P)API允許應用程序在無需連接到網絡和熱點的情況下連接到附近的設備。相比於藍牙技術,其具有加大的連接範圍。
配置應用權限
使用Wi-Fi P2P技術需要添加
CHANGE_WIFI_STATE
,ACCESS_WIFI_STATE
以及INTERNET
三種權限,因爲雖然Wi-Fi P2P技術可以不用訪問互聯網,但是它使用的是Java socket
的標準,所以需要INTERNET
權限。<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.nsdchat" ... <uses-permission android:required="true" android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:required="true" android:name="android.permission.CHANGE_WIFI_STATE"/> <uses-permission android:required="true" android:name="android.permission.INTERNET"/> ...
設置廣播接收器和P2P管理器
使用WiFi P2P時,需要偵聽事件發生時的broadcast intent。需要
IntentFilter
WIFI_P2P_STATE_CHANGED_ACTION
指示Wi-Fi P2P是否開啓WIFI_P2P_PEERS_CHANGED_ACTION
代表對等列表節點發生了變化。WIFI_P2P_CONNECTION_CHANGED_ACTION
表明Wi-Fi P2P連接狀態發生了變化。WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
指示設備詳細配置發生了變化。
private final IntentFilter intentFilter = new IntentFilter(); ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Indicates a change in the Wi-Fi P2P status. intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION); // Indicates a change in the list of available peers. intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION); // Indicates the state of Wi-Fi P2P connectivity has changed. intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION); // Indicates this device's details have changed. intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION); ... }
在
onCreate()
方法的最後,需要獲得WifiP2pManager
的實例,並調用他的initailize()
方法,以獲得WifiP2pManager.Channel
對象.Channel mChannel; @Override public void onCreate(Bundle savedInstanceState){ ... mManager = (WifiP2pManager)getSystemService(Context.WIFI_P2P_SERVICE); mChannel = mManager.initialize(this,getMainLooper(),null); }
然後創建廣播接收着,監聽上述不同的P2P狀態變化。
@Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) { // Determine if Wifi P2P mode is enabled or not, alert // the Activity. int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1); if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) { activity.setIsWifiP2pEnabled(true); } else { activity.setIsWifiP2pEnabled(false); } } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) { // The peer list has changed! We should probably do something about // that. } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) { // Connection state changed! We should probably do something about // that. } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) { DeviceListFragment fragment = (DeviceListFragment) activity.getFragmentManager() .findFragmentById(R.id.frag_list); fragment.updateThisDevice((WifiP2pDevice) intent.getParcelableExtra( WifiP2pManager.EXTRA_WIFI_P2P_DEVICE)); } }
並在Activity啓動時,註冊廣播,添加過濾器。Activity暫停或者關閉時候,註銷廣播。
//在Activity啓動後註冊廣播 @Override public void onResume() { super.onResume(); receiver = new WiFiDirectBroadcastReceiver(mManager, mChannel, this); registerReceiver(receiver, intentFilter); } //Activity關閉前,註銷廣播。 @Override public void onPause() { super.onPause(); unregisterReceiver(receiver); }
初始化對等節點發現(Peer Discovery)
調用
discoveryPeers()
開始搜尋附近設備,需要傳入參數- 上面得到的
WifiP2pManager.Channel
對象。 - 對
WifiP2pManager.ActionListener
接口的實現,確定發現成功與失敗時候的事件處理。
mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() { @Override public void onSuccess() { // Code for when the discovery initiation is successful goes here. // No services have actually been discovered yet, so this method // can often be left blank. Code for peer discovery goes in the // onReceive method, detailed below. } @Override public void onFailure(int reasonCode) { // Code for when the discovery initiation fails goes here. // Alert the user that something went wrong. } });
注意:如上僅僅完成了對匹配設備的發現掃描的初始化,
WifiP2pManager.ActionListener
中國年的方法會通知應用初始化是否正確等消息。- 上面得到的
獲取對等節點列表
完成初始化後,掃描會得到匹配的附近設備列表信息。需要實現
WifiP2pManager.PeerListener
接口。private List peers = new ArrayList();//匹配到的設備信息列表。 ... private PeerListListener peerListListener = new PeerListListener() { @Override public void onPeersAvailable(WifiP2pDeviceList peerList) { // Out with the old, in with the new. peers.clear(); peers.addAll(peerList.getDeviceList()); // If an AdapterView is backed by this data, notify it // of the change. For instance, if you have a ListView of available // peers, trigger an update. ((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged(); if (peers.size() == 0) { Log.d(WiFiDirectActivity.TAG, "No devices found"); return; } } }
如上獲得的匹配列表,我們需要將它傳遞給廣播接收者做進一步處理。
public void onReceive(Context context,Intent intent){ ... else 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 (mManager != null) { mManager.requestPeers(mChannel, peerListListener); } Log.d(WiFiDirectActivity.TAG, "P2P peers changed"); }... }
連接一個對等節點
發現到附近可用設備,則可以進一步的連接它,需要創建一個新的WifiP2pConfig對象,並將連接信息從設備WifiP2pDevice拷貝到其中,調用connect()方法。
@Override public void connect() { // Picking the first device found on the network. WifiP2pDevice device = peers.get(0); WifiP2pConfig config = new WifiP2pConfig(); config.deviceAddress = device.deviceAddress; config.wps.setup = WpsInfo.PBC; //ActionListener僅實現通知初始化成功與否 mManager.connect(mChannel, config, new ActionListener() { @Override public void onSuccess() { // WiFiDirectBroadcastReceiver will notify us. Ignore for now. } @Override public void onFailure(int reason) { Toast.makeText(WiFiDirectActivity.this, "Connect failed. Retry.", Toast.LENGTH_SHORT).show(); } }); }
使用
WifiP2pManager.ConnectionInfoListener
接口,onConnectionInfoAvailable()
來確定連接狀態。@Override public void onConnectionInfoAvailable(final WifiP2pInfo info) { // InetAddress from WifiP2pInfo struct. InetAddress groupOwnerAddress = info.groupOwnerAddress.getHostAddress()); // After the group negotiation, we can determine the group owner. if (info.groupFormed && info.isGroupOwner) { // Do whatever tasks are specific to the group owner. // One common case is creating a server thread and accepting // incoming connections. } else if (info.groupFormed) { // The other device acts as the client. In this case, // you'll want to create a client thread that connects to the group // owner. } }
完善廣播接收者的代碼,監聽到連接廣播信號時候,請求連接。
... } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) { if (mManager == null) { return; } NetworkInfo networkInfo = (NetworkInfo) intent .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO); if (networkInfo.isConnected()) { // We are connected with the other device, request connection // info to find group owner IP mManager.requestConnectionInfo(mChannel, connectionListener); } ...
3、使用WiFi P2P服務
第一節講述了NSD
服務用於局域網之間的連接通訊,本節的WiFi P2P有點類似,但是並不相同。
配置Manifest
需要網絡權限以及wifi相關權限。如上節所講的三個權限,配置在Android manifest清單文件中。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.nsdchat" ... <uses-permission android:required="true" android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:required="true" android:name="android.permission.CHANGE_WIFI_STATE"/> <uses-permission android:required="true" android:name="android.permission.INTERNET"/> ...
添加本地服務
需要在服務框架中註冊該服務,才能對外提供。
- 新建
WifiP2pServiceInfo
對象 - 加入相應的服務詳細信息
- 調用
addLocalService()
來註冊爲本地服務。
private void startRegistration() { // Create a string map containing information about your service. Map record = new HashMap(); record.put("listenport", String.valueOf(SERVER_PORT)); record.put("buddyname", "John Doe" + (int) (Math.random() * 1000)); record.put("available", "visible"); // Service information. Pass it an instance name, service type // _protocol._transportlayer , and the map containing // information other devices will want once they connect to this one. WifiP2pDnsSdServiceInfo serviceInfo = WifiP2pDnsSdServiceInfo.newInstance("_test", "_presence._tcp", record); // Add the local service, sending the service info, network channel, // and listener that will be used to indicate success or failure of // the request. mManager.addLocalService(channel, serviceInfo, new ActionListener() { @Override public void onSuccess() { // Command successful! Code isn't necessarily needed here, // Unless you want to update the UI or add logging statements. } @Override public void onFailure(int arg0) { // Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY } }); }
- 新建
發現附近的服務
新建一個
WifiP2pManager.DnsSdTxtRecordListener
實例來監聽實時收到到記錄。記錄到的周邊設備服務信息會拷貝到外部數據機構中,以供使用。final HashMap<String, String> buddies = new HashMap<String, String>(); ... private void discoverService() { DnsSdTxtRecordListener txtListener = new DnsSdTxtRecordListener() { @Override /* Callback includes: * fullDomain: full domain name: e.g "printer._ipp._tcp.local." * record: TXT record dta as a map of key/value pairs. * device: The device running the advertised service. */ public void onDnsSdTxtRecordAvailable( String fullDomain, Map record, WifiP2pDevice device) { Log.d(TAG, "DnsSdTxtRecord available -" + record.toString()); buddies.put(device.deviceAddress, record.get("buddyname")); } }; ... }
然後創建
WifiP2pManager.DnsSdServiceResponseListener
對象,來響應服務請求。上述兩個listener匹配構建後,調用setDnsResponseListener()
將它們加入WifiP2pManager
。private void discoverService() { ... DnsSdServiceResponseListener servListener = new DnsSdServiceResponseListener() { @Override public void onDnsSdServiceAvailable(String instanceName, String registrationType, WifiP2pDevice resourceType) { // Update the device name with the human-friendly version from // the DnsTxtRecord, assuming one arrived. resourceType.deviceName = buddies .containsKey(resourceType.deviceAddress) ? buddies .get(resourceType.deviceAddress) : resourceType.deviceName; // Add to the custom adapter defined specifically for showing // wifi devices. WiFiDirectServicesList fragment = (WiFiDirectServicesList) getFragmentManager() .findFragmentById(R.id.frag_peerlist); WiFiDevicesAdapter adapter = ((WiFiDevicesAdapter) fragment .getListAdapter()); adapter.add(resourceType); adapter.notifyDataSetChanged(); Log.d(TAG, "onBonjourServiceAvailable " + instanceName); } }; mManager.setDnsSdResponseListeners(channel, servListener, txtListener); ... }
調用
addServiceRequest()
創建服務請求,它需要一個listener來通知創建成功與否。serviceRequest = WifiP2pDnsSdServiceRequest.newInstance(); mManager.addServiceRequest(channel, serviceRequest, new ActionListener() { @Override public void onSuccess() { // Success! } @Override public void onFailure(int code) { // Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY } });
最後是調用
discoverService()
mManager.discoverServices(channel, new ActionListener() { @Override public void onSuccess() { // Success! } @Override public void onFailure(int code) { // Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY if (code == WifiP2pManager.P2P_UNSUPPORTED) { Log.d(TAG, "P2P isn't supported on this device."); else if(...) ... } });
順利的話,可以實現匹配連接的效果,常見錯誤代碼:
- P2P_UNSUPPORTED 當前設備不支持
- BUSY 系統繁忙
- ERROR 內部錯誤