네트워크 서비스 검색 사용

네트워크 서비스 검색(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)
            ...
        }
    }
    

자바

    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'로 설정합니다.

참고: 인터넷 할당 번호 관리 기관(Internet Assigned Numbers Authority, 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
            ...
        }
    }
    

자바

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

자바

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

자바

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

자바

    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)
    

자바

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

자바

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

    

자바

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