Sử dụng chức năng khám phá dịch vụ mạng

Tính năng khám phá dịch vụ mạng (NSD) cho phép ứng dụng của bạn truy cập vào các dịch vụ mà các thiết bị khác cung cấp trên mạng cục bộ. Các thiết bị hỗ trợ NSD bao gồm máy in, webcam, máy chủ HTTPS và các thiết bị di động khác.

NSD triển khai cơ chế Khám phá dịch vụ (DNS-SD) dựa trên DNS. Cơ chế này cho phép ứng dụng yêu cầu dịch vụ bằng cách chỉ định loại dịch vụ và tên của thực thể thiết bị cung cấp loại dịch vụ mong muốn. DNS-SD được hỗ trợ cả trên Android và trên các nền tảng di động khác.

Việc thêm NSD vào ứng dụng của bạn cho phép người dùng xác định các thiết bị khác trên mạng cục bộ có hỗ trợ các dịch vụ mà ứng dụng của bạn yêu cầu. Điều này rất hữu ích cho nhiều ứng dụng ngang hàng, chẳng hạn như chia sẻ tệp hoặc chơi trò chơi nhiều người chơi. API NSD của Android giúp đơn giản hoá những nỗ lực cần thiết để bạn triển khai các tính năng như vậy.

Bài học này sẽ hướng dẫn bạn cách tạo một ứng dụng có thể truyền tên và thông tin kết nối đến mạng cục bộ, đồng thời quét tìm thông tin từ các ứng dụng khác cũng thực hiện tương tự. Cuối cùng, bài học này sẽ hướng dẫn bạn cách kết nối với cùng một ứng dụng chạy trên một thiết bị khác.

Đăng ký dịch vụ của bạn trên mạng

Lưu ý: Bước này là không bắt buộc. Nếu không quan tâm đến việc truyền phát các dịch vụ của ứng dụng qua mạng cục bộ, bạn có thể chuyển đến phần tiếp theo, Khám phá các dịch vụ trên mạng.

Để đăng ký dịch vụ trên mạng cục bộ, trước tiên, hãy tạo một đối tượng NsdServiceInfo. Đối tượng này cung cấp thông tin mà các thiết bị khác trên mạng sử dụng khi quyết định xem có kết nối với dịch vụ của bạn hay không.

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

Đoạn mã này đặt tên dịch vụ thành "NsdChat". Tên dịch vụ là tên thực thể: đây là tên hiển thị cho các thiết bị khác trên mạng. Mọi thiết bị trên mạng đang sử dụng NSD đều có thể nhìn thấy tên này để tìm các dịch vụ cục bộ. Xin lưu ý rằng tên này phải là duy nhất cho mọi dịch vụ trên mạng và Android sẽ tự động xử lý việc giải quyết xung đột. Nếu hai thiết bị trên mạng đều cài đặt ứng dụng NsdChat, thì một trong số các thiết bị đó sẽ tự động thay đổi tên dịch vụ thành tên như "NsdChat (1)".

Tham số thứ hai sẽ đặt loại dịch vụ, chỉ định giao thức và lớp truyền tải mà ứng dụng sử dụng. Cú pháp là "_<protocol>._<transportlayer>". Trong đoạn mã, dịch vụ sử dụng giao thức HTTP chạy trên TCP. Ứng dụng cung cấp dịch vụ máy in (ví dụ: máy in mạng) sẽ đặt loại dịch vụ thành "_ipp._tcp".

Lưu ý: Cơ quan cấp phát số hiệu quốc tế (IANA) quản lý danh sách tập trung, đáng tin cậy về các loại dịch vụ được các giao thức khám phá dịch vụ (chẳng hạn như NSD và Bonjour) sử dụng. Bạn có thể tải danh sách xuống từ danh sách tên dịch vụ và số cổng của IANA. Nếu có ý định sử dụng một loại dịch vụ mới, bạn nên đặt trước bằng cách điền vào biểu mẫu đăng ký Dịch vụ và Cổng IANA.

Khi đặt cổng cho dịch vụ, hãy tránh mã hoá cứng cổng vì việc này xung đột với các ứng dụng khác. Ví dụ: giả sử ứng dụng luôn sử dụng cổng 1337 sẽ khiến ứng dụng có khả năng xung đột với các ứng dụng đã cài đặt khác sử dụng cùng một cổng. Thay vào đó, hãy sử dụng cổng có sẵn tiếp theo của thiết bị. Vì thông tin này được một dịch vụ truyền tin cung cấp cho các ứng dụng khác, nên các ứng dụng khác không cần biết cổng mà ứng dụng của bạn sử dụng tại thời điểm biên dịch. Thay vào đó, các ứng dụng có thể lấy thông tin này từ thông báo dịch vụ, ngay trước khi kết nối với dịch vụ của bạn.

Nếu bạn đang làm việc với ổ cắm, sau đây là cách bạn có thể khởi tạo ổ cắm cho bất kỳ cổng nào có sẵn chỉ bằng cách đặt giá trị đó thành 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();
    ...
}

Bây giờ, khi đã xác định được đối tượng NsdServiceInfo, bạn cần triển khai giao diện RegistrationListener. Giao diện này chứa các lệnh gọi lại mà Android dùng để thông báo cho ứng dụng về việc đăng ký và huỷ đăng ký dịch vụ thành công hay không thành công.

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

Giờ đây, bạn đã có tất cả thông tin để đăng ký dịch vụ. Gọi phương thức registerService().

Lưu ý rằng phương thức này không đồng bộ, vì vậy, mọi mã cần chạy sau khi dịch vụ được đăng ký đều phải nằm trong phương thức 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);
}

Khám phá các dịch vụ trên mạng

Mạng lưới này có vô số đời sống, từ máy in mạng kỳ thú cho đến webcam mạng linh hoạt cho đến những trận chiến khốc liệt, ác liệt của những người chơi cờ ca-rô ở gần đó. Khám phá dịch vụ là yếu tố then chốt để giúp ứng dụng của bạn thấy được hệ sinh thái chức năng sôi động này. Ứng dụng của bạn cần theo dõi các thông báo dịch vụ trên mạng để xem có những dịch vụ nào và lọc ra mọi dịch vụ mà ứng dụng không hoạt động được.

Quá trình khám phá dịch vụ, chẳng hạn như đăng ký dịch vụ, có hai bước: thiết lập trình nghe khám phá bằng các lệnh gọi lại có liên quan và thực hiện một lệnh gọi API không đồng bộ duy nhất đến discoverServices().

Trước tiên, hãy tạo thực thể cho một lớp ẩn danh sẽ triển khai NsdManager.DiscoveryListener. Đoạn mã sau đây cho thấy một ví dụ đơn giản:

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

API NSD sử dụng các phương thức trong giao diện này để thông báo cho ứng dụng của bạn khi quá trình khám phá bắt đầu, khi ứng dụng gặp lỗi và khi tìm thấy cũng như mất dịch vụ (bị mất có nghĩa là "không còn hoạt động"). Lưu ý rằng đoạn mã này sẽ thực hiện một số bước kiểm tra khi tìm thấy một dịch vụ.

  1. Tên dịch vụ của dịch vụ đã tìm thấy được so sánh với tên dịch vụ của dịch vụ cục bộ để xác định xem thiết bị vừa nhận thông báo truyền tin của riêng mình (là tên hợp lệ).
  2. Loại dịch vụ sẽ được kiểm tra để xác minh đây là loại dịch vụ mà ứng dụng của bạn có thể kết nối.
  3. Tên dịch vụ được kiểm tra để xác minh kết nối với đúng ứng dụng.

Không phải lúc nào bạn cũng cần kiểm tra tên dịch vụ và tên dịch vụ chỉ phù hợp nếu bạn muốn kết nối với một ứng dụng cụ thể. Ví dụ: ứng dụng có thể chỉ muốn kết nối với các phiên bản của chính nó đang chạy trên các thiết bị khác. Tuy nhiên, nếu ứng dụng muốn kết nối với máy in mạng, thì bạn chỉ cần thấy loại dịch vụ là "_ipp._tcp".

Sau khi thiết lập trình nghe, hãy gọi discoverServices(), truyền vào loại dịch vụ mà ứng dụng của bạn cần tìm, giao thức khám phá cần sử dụng và trình nghe bạn vừa tạo.

Kotlin

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

Java

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

Kết nối với các dịch vụ trên mạng

Khi tìm thấy một dịch vụ trên mạng để kết nối, trước tiên, ứng dụng của bạn phải xác định thông tin kết nối cho dịch vụ đó bằng cách sử dụng phương thức resolveService(). Hãy triển khai một NsdManager.ResolveListener để chuyển vào phương thức này rồi sử dụng phương thức này để nhận NsdServiceInfo chứa thông tin kết nối.

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

Sau khi dịch vụ được phân giải, ứng dụng của bạn sẽ nhận được thông tin chi tiết về dịch vụ, bao gồm cả địa chỉ IP và số cổng. Đây là mọi thứ bạn cần để tạo kết nối mạng riêng với dịch vụ.

Huỷ đăng ký dịch vụ khi đóng ứng dụng

Bạn cần phải bật và tắt chức năng NSD sao cho phù hợp trong suốt vòng đời của ứng dụng. Việc huỷ đăng ký ứng dụng khi ứng dụng đóng sẽ giúp ngăn các ứng dụng khác nghĩ rằng ứng dụng đó vẫn đang hoạt động và tìm cách kết nối với ứng dụng đó. Ngoài ra, khám phá dịch vụ là một thao tác tiêu tốn tài nguyên và phải được dừng khi Hoạt động gốc bị tạm dừng và được bật lại khi Hoạt động được tiếp tục. Ghi đè các phương thức vòng đời của Hoạt động chính và chèn mã để bắt đầu cũng như dừng truyền phát và dừng khám phá dịch vụ khi thích hợp.

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