네트워크 서비스 검색 사용

네트워크 서비스 검색 (NSD)을 사용하면 다른 기기가 로컬 네트워크에서 제공하는 서비스에 앱이 액세스할 수 있습니다. NSD를 지원하는 기기에는 프린터, 웹캠, HTTPS 서버, 기타 휴대기기가 포함됩니다.

NSD는 DNS 기반 서비스 검색 (DNS-SD) 메커니즘을 구현합니다. 이를 통해 앱에서 서비스 유형 및 원하는 서비스 유형을 제공하는 기기 인스턴스의 이름을 지정하여 서비스를 요청할 수 있습니다. DNS-SD는 Android와 다른 모바일 플랫폼 모두에서 지원됩니다.

NSD를 앱에 추가하면 사용자가 로컬 네트워크에서 앱이 요청하는 서비스를 지원하는 다른 기기를 식별할 수 있습니다. 이는 파일 공유 또는 멀티 플레이어 게임과 같은 다양한 P2P 애플리케이션에 유용합니다. 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 (International Assigned Numbers Authority)는 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
        ...
    }
}

자바

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 기능을 적절하게 사용 설정 및 사용 중지하는 것이 중요합니다. 애플리케이션이 종료될 때 등록을 취소하면 다른 애플리케이션이 애플리케이션이 여전히 활성 상태라고 판단하여 애플리케이션에 연결을 시도하는 것을 방지할 수 있습니다. 또한 서비스 검색은 비용이 많이 드는 작업이므로 상위 활동이 일시중지되면 중지되고 활동이 재개되면 다시 사용 설정해야 합니다. 기본 활동의 수명 주기 메서드를 재정의하고 코드를 삽입하여 서비스 브로드캐스트와 검색을 적절히 시작하고 중지합니다.

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