Android學習筆記第五篇--網絡連接與雲服務(一)

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 內部錯誤
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章