使用網路服務探索功能

網路服務探索 (NSD) 可讓應用程式存取其他裝置透過區域網路提供的服務。支援 NSD 的裝置包括印表機、網路攝影機、HTTPS 伺服器和其他行動裝置。

NSD 會實作 DNS 式服務探索 (DNS-SD) 機制,可讓應用程式指定提供服務類型及提供所需服務類型的裝置執行個體名稱,藉此要求服務。Android 和其他行動裝置平台都支援 DNS-SD。

在應用程式中新增 NSD,可讓您的使用者識別本機網路上支援您應用程式要求服務的其他裝置。對於各種點對點應用程式 (例如檔案共用或多人遊戲) 來說,這項功能非常實用。Android 的 NSD API 可簡化實作這類功能所需的工作。

本課程將說明如何建構應用程式,以便將其名稱和連線資訊廣播至區域網路,並掃描其他應用程式取得的資訊。最後,本課程將說明如何連線至在其他裝置上執行的相同應用程式。

在網路上註冊服務

注意 :此為選擇性步驟。如果您不在乎透過區域網路播送應用程式服務,可以直接跳到下一節:網路上的探索服務

如要在區域網路上註冊服務,請先建立 NsdServiceInfo 物件。這個物件會提供網路上的其他裝置在決定是否連線至您的服務時使用的資訊。

Kotlin

fun registerService(port: Int) {
    // Create the NsdServiceInfo object, and populate it.
    val serviceInfo = NsdServiceInfo().apply {
        // The name is subject to change based on conflicts
        // with other services advertised on the same network.
        serviceName = "NsdChat"
        serviceType = "_nsdchat._tcp"
        setPort(port)
        ...
    }
}

Java

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("_nsdchat._tcp");
    serviceInfo.setPort(port);
    ...
}

這段程式碼會將服務名稱設為「NsdChat」。服務名稱是執行個體名稱,是網路中其他裝置看到的名稱。凡是使用 NSD 尋找本機服務的網路裝置,都能看到這個名稱。請注意,網路上的任何服務名稱皆不得重複,且 Android 會自動處理衝突解決方案。如果網路上的兩部裝置都已安裝 NsdChat 應用程式,其中一部裝置會自動變更服務名稱,如「NsdChat (1)」類似。

第二個參數會設定服務類型,指定應用程式使用的通訊協定和傳輸層。語法為「_<protocol>._<transportlayer>」。在程式碼片段中,服務會使用透過 TCP 執行的 HTTP 通訊協定。提供印表機服務 (例如網路印表機) 的應用程式會將服務類型設為「_ipp._tcp」。

注意: 國際指派的號碼授權機構 (IANA) 可管理 NSD 和 Bonjour 等服務探索通訊協定使用的服務類型,並列為權威性且權威性清單。您可以從 IANA 服務名稱和通訊埠編號清單下載清單。如果您想使用新的服務類型,應填寫 IANA 通訊埠和服務註冊表單來保留服務類型。

為服務設定通訊埠時,請避免進行硬式編碼,因為這會與其他應用程式發生衝突。舉例來說,假設您的應用程式一律使用通訊埠 1337,就可能會與其他使用相同通訊埠的已安裝應用程式發生衝突。請改用裝置下一個可用的通訊埠。由於這項資訊是透過服務廣播功能提供給其他應用程式,因此應用程式在編譯期間並不需要其他應用程式所使用的通訊埠。應用程式可以在連線至您的服務之前,從您的服務廣播中取得這項資訊。

如果您使用通訊端,請按照以下方式將通訊端初始化為任何可用的通訊埠,並將其設為 0。

Kotlin

fun initializeServerSocket() {
    // Initialize a server socket on the next available port.
    serverSocket = ServerSocket(0).also { socket ->
        // Store the chosen port.
        mLocalPort = socket.localPort
        ...
    }
}

Java

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

    // Store the chosen port.
    localPort = serverSocket.getLocalPort();
    ...
}

您已定義 NsdServiceInfo 物件,接著必須實作 RegistrationListener 介面。此介麵包含 Android 用來提醒應用程式註冊成功或失敗及取消註冊的回呼。

Kotlin

private val registrationListener = object : NsdManager.RegistrationListener {

    override fun 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.serviceName
    }

    override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
        // Registration failed! Put debugging code here to determine why.
    }

    override fun onServiceUnregistered(arg0: NsdServiceInfo) {
        // Service has been unregistered. This only happens when you call
        // NsdManager.unregisterService() and pass in this listener.
    }

    override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
        // Unregistration failed. Put debugging code here to determine why.
    }
}

Java

public void initializeRegistrationListener() {
    registrationListener = 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.
            serviceName = 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.
        }
    };
}

現在您已完成註冊服務的所有步驟。呼叫 registerService() 方法。

請注意,這是非同步的,因此任何需要在註冊服務之後執行的程式碼,都必須在 onServiceRegistered() 方法中。

Kotlin

fun registerService(port: Int) {
    // Create the NsdServiceInfo object, and populate it.
    val serviceInfo = NsdServiceInfo().apply {
        // The name is subject to change based on conflicts
        // with other services advertised on the same network.
        serviceName = "NsdChat"
        serviceType = "_nsdchat._tcp"
        setPort(port)
    }

    nsdManager = (getSystemService(Context.NSD_SERVICE) as NsdManager).apply {
        registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
    }
}

Java

public void registerService(int port) {
    NsdServiceInfo serviceInfo = new NsdServiceInfo();
    serviceInfo.setServiceName("NsdChat");
    serviceInfo.setServiceType("_http._tcp.");
    serviceInfo.setPort(port);

    nsdManager = Context.getSystemService(Context.NSD_SERVICE);

    nsdManager.registerService(
            serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener);
}

瀏覽網路上的服務

這個網路充滿生命體,從野獸網路印表機、流利的網路網路攝影機,到鄰近的井字遊戲玩家的巧妙戰鬥。要讓應用程式看見這個充滿活力的功能生態系統,關鍵在於服務探索。您的應用程式需要監聽網路上的服務廣播,看看有哪些服務可以使用,並篩除應用程式無法使用的任何服務。

服務探索 (例如登錄服務) 有兩個步驟:使用相關回呼設定探索事件監聽器,以及對 discoverServices() 進行單一非同步 API 呼叫。

首先,請將實作 NsdManager.DiscoveryListener 的匿名類別例項化。以下程式碼片段為簡易範例:

Kotlin

// Instantiate a new DiscoveryListener
private val discoveryListener = object : NsdManager.DiscoveryListener {

    // Called as soon as service discovery begins.
    override fun onDiscoveryStarted(regType: String) {
        Log.d(TAG, "Service discovery started")
    }

    override fun onServiceFound(service: NsdServiceInfo) {
        // A service was found! Do something with it.
        Log.d(TAG, "Service discovery success$service")
        when {
            service.serviceType != SERVICE_TYPE -> // Service type is the string containing the protocol and
                // transport layer for this service.
                Log.d(TAG, "Unknown Service Type: ${service.serviceType}")
            service.serviceName == 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")
            service.serviceName.contains("NsdChat") -> nsdManager.resolveService(service, resolveListener)
        }
    }

    override fun onServiceLost(service: NsdServiceInfo) {
        // When the network service is no longer available.
        // Internal bookkeeping code goes here.
        Log.e(TAG, "service lost: $service")
    }

    override fun onDiscoveryStopped(serviceType: String) {
        Log.i(TAG, "Discovery stopped: $serviceType")
    }

    override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
        Log.e(TAG, "Discovery failed: Error code:$errorCode")
        nsdManager.stopServiceDiscovery(this)
    }

    override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
        Log.e(TAG, "Discovery failed: Error code:$errorCode")
        nsdManager.stopServiceDiscovery(this)
    }
}

Java

public void initializeDiscoveryListener() {

    // Instantiate a new DiscoveryListener
    discoveryListener = 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(serviceName)) {
                // 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: " + serviceName);
            } else if (service.getServiceName().contains("NsdChat")){
                nsdManager.resolveService(service, resolveListener);
            }
        }

        @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);
            nsdManager.stopServiceDiscovery(this);
        }

        @Override
        public void onStopDiscoveryFailed(String serviceType, int errorCode) {
            Log.e(TAG, "Discovery failed: Error code:" + errorCode);
            nsdManager.stopServiceDiscovery(this);
        }
    };
}

NSD API 會在探索功能啟動、失敗、找到服務並遺失時,使用這個介面中的方法通知應用程式 (遺失代表「已無法使用」)。請注意,這個程式碼片段會在發現服務時執行幾項檢查。

  1. 系統會將找到的服務名稱與「本機服務」的名稱比對,判斷裝置是否已自行擷取廣播 (這是有效的值)。
  2. 已勾選服務類型,以確認應用程式可連線的服務類型。
  3. 系統會檢查服務名稱,以驗證與正確應用程式的連線。

您不一定要檢查服務名稱,且只有在您想連線至特定應用程式時才需要檢查服務名稱。例如,應用程式可能只想連線至在其他裝置上執行的執行個體。不過,如果應用程式想要連線至網路印表機,只要知道服務類型是「_ipp._tcp」即可。

設定事件監聽器後,請呼叫 discoverServices(),傳入應用程式應尋找的服務類型、要使用的探索通訊協定,以及您剛建立的事件監聽器。

Kotlin

nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)

Java

nsdManager.discoverServices(
        SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);

連線至網路上的服務

當應用程式在網路上找到要連線的服務時,必須先使用 resolveService() 方法判斷該服務的連線資訊。實作要傳入這個方法的 NsdManager.ResolveListener,並使用該方法取得包含連線資訊的 NsdServiceInfo

Kotlin

private val resolveListener = object : NsdManager.ResolveListener {

    override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
        // Called when the resolve fails. Use the error code to debug.
        Log.e(TAG, "Resolve failed: $errorCode")
    }

    override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
        Log.e(TAG, "Resolve Succeeded. $serviceInfo")

        if (serviceInfo.serviceName == mServiceName) {
            Log.d(TAG, "Same IP.")
            return
        }
        mService = serviceInfo
        val port: Int = serviceInfo.port
        val host: InetAddress = serviceInfo.host
    }
}

Java

public void initializeResolveListener() {
    resolveListener = 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(serviceName)) {
                Log.d(TAG, "Same IP.");
                return;
            }
            mService = serviceInfo;
            int port = mService.getPort();
            InetAddress host = mService.getHost();
        }
    };
}

服務解析後,應用程式會收到詳細的服務資訊,包括 IP 位址和通訊埠號碼。這就是建立自己服務網路連線所需的一切。

應用程式關閉時取消註冊服務

請務必在應用程式的生命週期中適當啟用及停用 NSD 功能。在應用程式關閉時取消註冊,可防止其他應用程式誤認為應用程式仍處於使用中狀態,並嘗試與其連線。此外,服務探索所費不貲,且應在父項 Activity 暫停時停止,並在活動恢復後重新啟用。覆寫主要活動的生命週期方法,並視情況插入程式碼,以開始和停止服務廣播和探索。

Kotlin

    // In your application's Activity

    override fun onPause() {
        nsdHelper?.tearDown()
        super.onPause()
    }

    override fun onResume() {
        super.onResume()
        nsdHelper?.apply {
            registerService(connection.localPort)
            discoverServices()
        }
    }

    override fun onDestroy() {
        nsdHelper?.tearDown()
        connection.tearDown()
        super.onDestroy()
    }

    // NsdHelper's tearDown method
    fun tearDown() {
        nsdManager.apply {
            unregisterService(registrationListener)
            stopServiceDiscovery(discoveryListener)
        }
    }

Java

    // In your application's Activity

    @Override
    protected void onPause() {
        if (nsdHelper != null) {
            nsdHelper.tearDown();
        }
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (nsdHelper != null) {
            nsdHelper.registerService(connection.getLocalPort());
            nsdHelper.discoverServices();
        }
    }

    @Override
    protected void onDestroy() {
        nsdHelper.tearDown();
        connection.tearDown();
        super.onDestroy();
    }

    // NsdHelper's tearDown method
    public void tearDown() {
        nsdManager.unregisterService(registrationListener);
        nsdManager.stopServiceDiscovery(discoveryListener);
    }