استخدام ميزة "رصد خدمات الشبكة"

يمنح "اكتشاف خدمة الشبكة" (NSD) تطبيقك إمكانية الوصول إلى الخدمات التي تقدّمها الأجهزة الأخرى على الشبكة المحلية. تشمل الأجهزة التي تدعم NSD الطابعات وكاميرات الويب وخوادم HTTPS والأجهزة المحمولة الأخرى.

وينفِّذ NSD آلية اكتشاف الخدمة المستندة إلى نظام أسماء النطاقات (DNS-SD)، والتي تسمح لتطبيقك بطلب الخدمات من خلال تحديد نوع الخدمة واسم مثيل الجهاز الذي يوفّر نوع الخدمة المطلوب. يتم دعم DNS-SD على كل من Android والأنظمة الأساسية الأخرى للأجهزة المحمولة.

إنّ إضافة NSD إلى تطبيقك تسمح للمستخدمين بتحديد الأجهزة الأخرى المتاحة على الشبكة المحلية والتي تتوافق مع الخدمات التي يطلبها تطبيقك. وهذا مفيد لمجموعة متنوعة من التطبيقات من نظير إلى نظير مثل مشاركة الملفات أو الألعاب متعددة اللاعبين. تعمل واجهات برمجة التطبيقات NSD في Android على تبسيط الجهود المطلوبة لتنفيذ هذه الميزات.

يشرح لك هذا الدرس كيفية إنشاء تطبيق يمكنه بث اسمه ومعلومات الاتصال بالشبكة المحلية والبحث عن المعلومات من التطبيقات الأخرى التي تفعل الشيء نفسه. أخيرًا، يوضح لك هذا الدرس كيفية الاتصال بالتطبيق ذاته الذي يتم تشغيله على جهاز آخر.

تسجيل الخدمة على الشبكة

ملاحظة: هذه الخطوة اختيارية. إذا كنت لا تهتم ببث خدمات تطبيقك عبر الشبكة المحلية، يمكنك الانتقال إلى القسم التالي، اكتشاف الخدمات على الشبكة.

لتسجيل الخدمة على الشبكة المحلية، يجب أولاً إنشاء عنصر 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>". في مقتطف الرمز، تستخدم الخدمة بروتوكول HTTP يتم تشغيله عبر بروتوكول TCP. سيضبط التطبيق الذي يقدم خدمة طابعة (على سبيل المثال، طابعة الشبكة) نوع الخدمة على " _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);
}

اكتشاف الخدمات على الشبكة

تمتلئ الشبكة بالحياة، بدءًا من طابعات الشبكة المزعجة، ووصولاً إلى كاميرات الويب السهلة الاستخدام، وحتى المعارك الشرسة التي يخوضها لاعبو tic-tac-toe القريبون. إنّ اكتشاف الخدمة هو الأساس للسماح لتطبيقك بالاطّلاع على هذه المنظومة المتكاملة من الوظائف. يحتاج تطبيقك إلى الاستماع إلى عمليات بث الخدمة على الشبكة لمعرفة الخدمات المتاحة وفلترة أي شيء لا يمكن للتطبيق العمل معه.

تتكوّن ميزة اكتشاف الخدمة، مثل تسجيل الخدمة، من خطوتَين: إعداد أداة رصد لرصد عمليات البحث من خلال طلبات معاودة الاتصال ذات الصلة، وإجراء طلب واحد غير متزامن من واجهة برمجة التطبيقات مع واجهة برمجة التطبيقات discoverServices()

أولاً، أنشئ مثيلاً لفئة مجهولة المصدر تنفّذ 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 الطرق المتوفرة في هذه الواجهة لإبلاغ تطبيقك عند بدء الاكتشاف، وعند تعذُّره، وعند العثور على الخدمات وفقدانها (تعني "مفقودة" "لم تعد متاحة"). لاحظ أن هذا المقتطف يجري عدة عمليات تحقق عند العثور على خدمة.

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