Android使用網(wǎng)絡(luò)服務(wù)發(fā)現(xiàn)

2018-08-02 17:40 更新

編寫:naizhengtan - 原文:http://developer.android.com/training/connect-devices-wirelessly/nsd.html

添加網(wǎng)絡(luò)服務(wù)發(fā)現(xiàn)(Network Service Discovery)到我們的 app 中,可以使我們的用戶辨識在局域網(wǎng)內(nèi)支持我們的 app 所請求的服務(wù)的設(shè)備。這種技術(shù)在點對點應(yīng)用中能夠提供大量幫助,例如文件共享、聯(lián)機游戲等。Android 的網(wǎng)絡(luò)服務(wù)發(fā)現(xiàn)(NSD)API 大大降低實現(xiàn)上述功能的難度。

本講將簡要介紹如何創(chuàng)建 NSD 應(yīng)用,使其能夠在本地網(wǎng)絡(luò)內(nèi)廣播自己的名稱和連接信息,并且掃描其它正在做同樣事情的應(yīng)用信息。最后,將介紹如何連接運行著同樣應(yīng)用的另一臺設(shè)備。

注冊 NSD 服務(wù)

Note: 這一步驟是選做的。如果我們并不關(guān)心在本地網(wǎng)絡(luò)上廣播 app 服務(wù),那么我們可以跳過這一步,直接嘗試發(fā)現(xiàn)網(wǎng)絡(luò)中的服務(wù)。

在局域網(wǎng)內(nèi)注冊自己服務(wù)的第一步是創(chuàng)建 NsdServiceInfo 對象。此對象包含的信息能夠幫助網(wǎng)絡(luò)中的其他設(shè)備決定是否要連接到我們所提供的服務(wù)。

public void registerService(int port) {
    // Create the NsdServiceInfo object, and populate it.
    NsdServiceInfo serviceInfo  = new NsdServiceInfo();

    // The name is subject to change based on conflicts
    // with other services advertised on the same network.
    serviceInfo.setServiceName("NsdChat");
    serviceInfo.setServiceType("_http._tcp.");
    serviceInfo.setPort(port);
    ....
}

這段代碼將服務(wù)命名為“NsdChat”。該名稱將對所有局域網(wǎng)絡(luò)中使用 NSD 查找本地服務(wù)的設(shè)備可見。需要注意的是,在網(wǎng)絡(luò)內(nèi)該名稱必須是獨一無二的。Android 系統(tǒng)會自動處理沖突的服務(wù)名稱。如果同時有兩個名為“NsdChat”的應(yīng)用,其中一個會被自動轉(zhuǎn)換為類似“NsdChat(1)”這樣的名稱。

第二個參數(shù)設(shè)置了服務(wù)類型,即指定應(yīng)用使用的協(xié)議和傳輸層。語法是“_< protocol >._< transportlayer >”。在上面的代碼中,服務(wù)使用了TCP協(xié)議上的HTTP協(xié)議。想要提供打印服務(wù)(例如,一臺網(wǎng)絡(luò)打印機)的應(yīng)用應(yīng)該將服務(wù)的類型設(shè)置為“_ipp._tcp”。

Note: 互聯(lián)網(wǎng)編號分配機構(gòu)(International Assigned Numbers Authority,簡稱 IANA)提供用于服務(wù)發(fā)現(xiàn)協(xié)議(例如 NSD 和 Bonjour)的官方服務(wù)種類列表。我們可以下載該列表了解相應(yīng)的服務(wù)名稱和端口號碼。如果我們想起用新的服務(wù)種類,應(yīng)該向 IANA 官方提交申請。

當(dāng)為我們的服務(wù)設(shè)置端口號時,應(yīng)該盡量避免將其硬編碼在代碼中,以防止與其他應(yīng)用產(chǎn)生沖突。例如,如果我們的應(yīng)用僅僅使用端口1337,就可能與其他使用1337端口的應(yīng)用發(fā)生沖突。解決方法是,不要硬編碼,使用下一個可用的端口。不必?fù)?dān)心其他應(yīng)用無法知曉服務(wù)的端口號,因為該信息將包含在服務(wù)的廣播包中。接收到廣播后,其他應(yīng)用將從廣播包中得知服務(wù)端口號,并通過端口連接到我們的服務(wù)上。

如果使用的是 socket,那么我們可以將端口設(shè)置為 0,來初始化 socket 到任意可用的端口。

public void initializeServerSocket() {
    // Initialize a server socket on the next available port.
    mServerSocket = new ServerSocket(0);

    // Store the chosen port.
    mLocalPort =  mServerSocket.getLocalPort();
    ...
}

現(xiàn)在,我們已經(jīng)成功的創(chuàng)建了 NsdServiceInfo 對象,接下來要做的是實現(xiàn) RegistrationListener 接口。該接口包含了注冊在 Android 系統(tǒng)中的回調(diào)函數(shù),作用是通知應(yīng)用程序服務(wù)注冊和注銷的成功或者失敗。

public void initializeRegistrationListener() {
    mRegistrationListener = new NsdManager.RegistrationListener() {

        @Override
        public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {
            // Save the service name.  Android may have changed it in order to
            // resolve a conflict, so update the name you initially requested
            // with the name Android actually used.
            mServiceName = NsdServiceInfo.getServiceName();
        }

        @Override
        public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
            // Registration failed!  Put debugging code here to determine why.
        }

        @Override
        public void onServiceUnregistered(NsdServiceInfo arg0) {
            // Service has been unregistered.  This only happens when you call
            // NsdManager.unregisterService() and pass in this listener.
        }

        @Override
        public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
            // Unregistration failed.  Put debugging code here to determine why.
        }
    };
}

萬事俱備只欠東風(fēng),調(diào)用 registerService() 方法,真正注冊服務(wù)。

因為該方法是異步的,所以在服務(wù)注冊之后的操作都需要在 onServiceRegistered() 方法中進(jìn)行。

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);
}

發(fā)現(xiàn)網(wǎng)絡(luò)中的服務(wù)

網(wǎng)絡(luò)充斥著我們的生活,從網(wǎng)絡(luò)打印機到網(wǎng)絡(luò)攝像頭,再到聯(lián)網(wǎng)井字棋。網(wǎng)絡(luò)服務(wù)發(fā)現(xiàn)是能讓我們的應(yīng)用融入這一切功能的關(guān)鍵。我們的應(yīng)用需要偵聽網(wǎng)絡(luò)內(nèi)服務(wù)的廣播,發(fā)現(xiàn)可用的服務(wù),過濾無效的信息。

與注冊網(wǎng)絡(luò)服務(wù)類似,服務(wù)發(fā)現(xiàn)需要兩步驟:用相應(yīng)的回調(diào)函數(shù)設(shè)置發(fā)現(xiàn)監(jiān)聽器(Discover Listener),以及調(diào)用 discoverServices() 這個異步API。

首先,實例化一個實現(xiàn) NsdManager.DiscoveryListener 接口的匿名類。下列代碼是一個簡單的范例:

public void initializeDiscoveryListener() {

    // Instantiate a new DiscoveryListener
    mDiscoveryListener = new NsdManager.DiscoveryListener() {

        //  Called as soon as service discovery begins.
        @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 通過使用該接口中的方法通知用戶程序發(fā)現(xiàn)何時開始、何時失敗以及何時找到可用服務(wù)和何時服務(wù)丟失(丟失意味著“不再可用”)。在上述代碼中,當(dāng)發(fā)現(xiàn)了可用的服務(wù)時,程序做了幾次檢查。

  1. 比較找到服務(wù)的名稱與本地服務(wù)的名稱,判斷設(shè)備是否獲得自己的(合法的)廣播。
  2. 檢查服務(wù)的類型,確認(rèn)這個類型我們的應(yīng)用是否可以接入。
  3. 檢查服務(wù)的名稱,確認(rèn)是否接入了正確的應(yīng)用。

我們并不需要每次都檢查服務(wù)名稱,僅當(dāng)我們想要接入特定的應(yīng)用時需要檢查。例如,應(yīng)用只想與運行在其他設(shè)備上的相同應(yīng)用通信。然而,如果應(yīng)用僅僅想接入到一臺網(wǎng)絡(luò)打印機,那么看到服務(wù)類型是“_ipp._tcp”的服務(wù)就足夠了。

當(dāng)配置好監(jiān)聽器后,調(diào)用 discoverService() 函數(shù),其參數(shù)包括試圖發(fā)現(xiàn)的服務(wù)種類、發(fā)現(xiàn)使用的協(xié)議、以及上一步創(chuàng)建的監(jiān)聽器。

mNsdManager.discoverServices(
        SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);

連接到網(wǎng)絡(luò)上的服務(wù)

當(dāng)我們的應(yīng)用發(fā)現(xiàn)了網(wǎng)上可接入的服務(wù),首先需要調(diào)用 resolveService() 方法,以確定服務(wù)的連接信息。實現(xiàn) NsdManager.ResolveListener 對象并將其傳入 resolveService() 方法,并使用這個 NsdManager.ResolveListener 對象獲得包含連接信息的 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();
        }
    };
}

當(dāng)服務(wù)解析完成后,我們將獲得服務(wù)的詳細(xì)資料,包括其 IP 地址和端口號。此時,我們就可以創(chuàng)建自己網(wǎng)絡(luò)連接與服務(wù)進(jìn)行通訊。

當(dāng)程序退出時注銷服務(wù)

在應(yīng)用的生命周期中正確的開啟和關(guān)閉 NSD 服務(wù)是十分關(guān)鍵的。在程序退出時注銷服務(wù)可以防止其他程序因為不知道服務(wù)退出而反復(fù)嘗試連接的行為。另外,服務(wù)發(fā)現(xiàn)是一種開銷很大的操作,應(yīng)該隨著父 Activity 的暫停而停止,當(dāng)用戶返回該界面時再開啟。因此,開發(fā)者應(yīng)該重寫 Activity 的生命周期函數(shù),并添加按照需要開啟和停止服務(wù)廣播和發(fā)現(xiàn)的代碼。

//In your application's 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);
    }


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號