Try  HackMD Logo HackMD

mDNS 與 DNS-SD 實作

本實作主要參考 nsdchat,出處應來自 Android Developers 官方,但據下方文章作者提及官方好像已經移除且也找不到了,這是舊的連結,不過官方依然有 sample code 只是比較簡化就是。

本人參考後無法正常運行,且範例是沒有區別 server 與 client ,在 trace 過程中有點難理解,故自行簡化重新修改成兩種版本:dns_serverdns_client,可用兩台不同裝置分別安裝來測試看看:

兩支不同專案實作時,記得在 AndroidManifest.xml 加上
<uses-permission android:name="android.permission.INTERNET"/>

dns_server

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/tv_state" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/btn_registger" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:textAllCaps="false" android:text="Register Service"/> <Button android:id="@+id/btn_unregistger" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/btn_registger" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:text="Unregister Service" android:textAllCaps="false"/> </androidx.constraintlayout.widget.ConstraintLayout>
  • MainActivity
package com.example.nsdchat_server_test; import androidx.appcompat.app.AppCompatActivity; import android.content.Context; import android.net.nsd.NsdManager; import android.net.nsd.NsdServiceInfo; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity { //region Properties public static final String TAG = "MainActivity"; public static final String SERVICE_NAME = "NsdChat_server"; // 要註冊的 service 名稱 public static final String SERVICE_TYPE = "_http._tcp."; // 要註冊的 service 類型 public static final int SERVICE_PORT = 2222; // 要註冊的 service port private TextView tv_state; private Button btn_registger; private Button btn_unregistger; private NsdManager mNsdManager; private static NsdServiceInfo mServiceInfo; private NsdManager.RegistrationListener mRegistrationListener; //endregion @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mNsdManager = (NsdManager) getSystemService(Context.NSD_SERVICE); initView(); initListener(); } @Override protected void onStop() { tearDown(); super.onStop(); } private void initView() { tv_state = (TextView) findViewById(R.id.tv_state); btn_registger = (Button) findViewById(R.id.btn_registger); btn_unregistger = (Button) findViewById(R.id.btn_unregistger); } private void initListener() { btn_registger.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (mServiceInfo != null) { updateRegisterStateUI("您已註冊過 service: " + mServiceInfo.getServiceName()); return; } NsdServiceInfo serviceInfo = createServiceInfo(); registerService(serviceInfo); } }); btn_unregistger.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { tearDown(); } }); } private void registerService(NsdServiceInfo serviceInfo) { mRegistrationListener = new NsdManager.RegistrationListener() { @Override public void onServiceRegistered(NsdServiceInfo nsdServiceInfo) { String serviceName = nsdServiceInfo.getServiceName(); Log.d(TAG, "Service registered: " + serviceName); mServiceInfo = nsdServiceInfo; updateRegisterStateUI("註冊 service 成功: " + serviceName); } @Override public void onRegistrationFailed(NsdServiceInfo nsdServiceInfo, int i) { Log.d(TAG, "Service registration failed: " + i); updateRegisterStateUI("Service registration failed"); } @Override public void onServiceUnregistered(NsdServiceInfo nsdServiceInfo) { Log.d(TAG, "Service unregistered: " + nsdServiceInfo.getServiceName()); updateRegisterStateUI("您已註銷 service"); } @Override public void onUnregistrationFailed(NsdServiceInfo nsdServiceInfo, int i) { Log.d(TAG, "Service unregistration failed: " + i); updateRegisterStateUI("Service unregistration failed"); } }; mNsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener); } private NsdServiceInfo createServiceInfo() { NsdServiceInfo serviceInfo = new NsdServiceInfo(); serviceInfo.setServiceName(SERVICE_NAME); serviceInfo.setServiceType(SERVICE_TYPE); serviceInfo.setPort(SERVICE_PORT); return serviceInfo; } private void tearDown() { if (mRegistrationListener != null) { try { mNsdManager.unregisterService(mRegistrationListener); } finally { } mRegistrationListener = null; mServiceInfo = null; } } private void updateRegisterStateUI(String msg) { new Thread(new Runnable() { @Override public void run() { tv_state.post(new Runnable() { @Override public void run() { tv_state.setText(msg); } }); } }).start(); } }

dns_client

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/cl_serviceName" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="16dp"> <TextView android:id="@+id/tv_serviceName_title" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" android:textAllCaps="false" android:text="Search Service Name: " android:gravity="start"/> <TextView android:id="@+id/tv_serviceName" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toEndOf="@id/tv_serviceName_title" app:layout_constraintEnd_toEndOf="parent" android:textAllCaps="false" android:textStyle="bold" android:text="SERVICE NAME"/> </androidx.constraintlayout.widget.ConstraintLayout> <Button android:id="@+id/btn_discovery" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/cl_serviceName" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="16dp" android:textAllCaps="false" android:text="Discovery"/> <Button android:id="@+id/btn_connectService" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/btn_discovery" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:textAllCaps="false" android:lines="3" android:text="Connect service\n(or Bind Service)\n(or Resolve Service)"/> <Button android:id="@+id/btn_disconnect" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/btn_connectService" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:textAllCaps="false" android:text="Disconnect"/> <ScrollView android:layout_width="0dp" android:layout_height="0dp" android:layout_margin="16dp" app:layout_constraintTop_toBottomOf="@id/btn_disconnect" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@id/cl_connectedState" android:background="#B5B5B5"> <TextView android:id="@+id/tv_state" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </ScrollView> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/cl_connectedState" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="16dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"> <TextView android:id="@+id/tv_connectedState_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Connect state:" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"/> <TextView android:id="@+id/tv_connectedState" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="16dp" app:layout_constraintTop_toBottomOf="@id/tv_connectedState_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:textColor="#FF0000" android:lines="5"/> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
  • MainActivity
package com.example.nsdchat_client_test; import androidx.appcompat.app.AppCompatActivity; import android.content.Context; import android.net.nsd.NsdManager; import android.net.nsd.NsdServiceInfo; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity { //region Properties public static final String TAG = "MainActivity"; public static final String FIND_SERVICE_NAME = "NsdChat_server"; // 要尋找的 service 名稱 public static final String FIND_SERVICE_TYPE = "_http._tcp."; // 要尋找的 service 類型 private TextView tv_serviceName; private TextView tv_state; private Button btn_discovery; private Button btn_connectService; private Button btn_disconnect; private TextView tv_connectedState; private NsdManager mNsdManager; private NsdManager.ResolveListener mResolveListener; private NsdManager.DiscoveryListener mDiscoveryListener; private static NsdServiceInfo mTryConnServiceInfo; // 符合 service 名稱可以嘗試 connect 的 service private static NsdServiceInfo mConnServiceInfo; // 已 connect 的 service //endregion @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mNsdManager = (NsdManager) getSystemService(Context.NSD_SERVICE); initView(); initListener(); tv_serviceName.setText(FIND_SERVICE_NAME); } private void initView() { tv_serviceName = (TextView) findViewById(R.id.tv_serviceName); tv_state = (TextView) findViewById(R.id.tv_state); btn_discovery = (Button) findViewById(R.id.btn_discovery); btn_connectService = (Button) findViewById(R.id.btn_connectService); btn_disconnect = (Button) findViewById(R.id.btn_disconnect); tv_connectedState = (TextView) findViewById(R.id.tv_connectedState); } private void initListener() { //region Discovery 按鈕 btn_discovery.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { stopDiscovery(); tv_state.setText(""); tv_connectedState.setText(""); startDiscovery(); } }); //endregion //region Connect Service 按鈕 btn_connectService.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // 要找的 service name 不存在 if (mTryConnServiceInfo == null) { tv_connectedState.setText("service name not exist."); return; } // 已存在 connected service else if (mConnServiceInfo != null) { String msg = getConnectedServiceInfo(mConnServiceInfo); tv_connectedState.setText("已存在 connected service: " + "\n" + msg); return; } startConnectService(mTryConnServiceInfo); } }); //endregion //region Disconnect 按鈕 btn_disconnect.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { tv_connectedState.setText(""); mTryConnServiceInfo = null; mConnServiceInfo = null; // 刷新列表 reloadDiscovery(); } }); //endregion } private void startDiscovery() { mDiscoveryListener = new NsdManager.DiscoveryListener() { /** 開始搜尋 service */ @Override public void onDiscoveryStarted(String regType) { Log.d(TAG, "Service discovery started"); } /** 搜尋 service 中 */ @Override public void onServiceFound(NsdServiceInfo service) { Log.d(TAG, "Service discovery success: " + service); if (!service.getServiceType().equals(FIND_SERVICE_TYPE)) { Log.d(TAG, "Unknown Service Type: " + service.getServiceType()); } String list_msg; // 若已存在 connected service, 且 service 名字相同 (目前使用名稱來判斷 service 是否相同) if (mConnServiceInfo != null && service.getServiceName().equals(FIND_SERVICE_NAME)) { list_msg = String.valueOf(service) + " (已連接)" + "\n\n"; } else { list_msg = String.valueOf(service) + "\n\n"; } updateDiscoveryStateUI(list_msg); if (service.getServiceName().equals(FIND_SERVICE_NAME)) { mTryConnServiceInfo = service; } } /** service 斷線 */ @Override public void onServiceLost(NsdServiceInfo service) { Log.e(TAG, "service lost: " + service); if (mConnServiceInfo != null) { tv_connectedState.setText("service lost"); mTryConnServiceInfo = null; mConnServiceInfo = null; } // 刷新列表 reloadDiscovery(); } /** 停止搜尋 service */ @Override public void onDiscoveryStopped(String serviceType) { Log.i(TAG, "Discovery stopped: " + serviceType); } /** 開始搜尋 service fialed */ @Override public void onStartDiscoveryFailed(String serviceType, int errorCode) { Log.e(TAG, "Discovery failed: Error code:" + errorCode); } /** 停止搜尋 service failed */ @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { Log.e(TAG, "Discovery failed: Error code:" + errorCode); } }; mNsdManager.discoverServices(FIND_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener); } private void startConnectService(NsdServiceInfo service) { mResolveListener = new NsdManager.ResolveListener() { /** connect service 失敗 */ @Override public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { Log.e(TAG, "Resolve failed" + errorCode); tv_connectedState.setText("Resolve failed: " + serviceInfo.getServiceName()); } /** connect service 成功 */ @Override public void onServiceResolved(NsdServiceInfo serviceInfo) { Log.e(TAG, "Resolve Succeeded. " + serviceInfo); mConnServiceInfo = serviceInfo; // 刷新 Connect state String msg = getConnectedServiceInfo(serviceInfo); tv_connectedState.setText("Connect Succeeded" + "\n" + msg); // 刷新列表 reloadDiscovery(); } }; mNsdManager.resolveService(service, mResolveListener); } private void stopDiscovery() { if (mDiscoveryListener != null) { try { mNsdManager.stopServiceDiscovery(mDiscoveryListener); } finally { } mDiscoveryListener = null; } } private void updateDiscoveryStateUI(String msg) { new Thread(new Runnable() { @Override public void run() { tv_state.post(new Runnable() { @Override public void run() { tv_state.append(msg); } }); } }).start(); } /** * 重新刷新搜尋 service 列表 */ private void reloadDiscovery() { new Thread(new Runnable() { @Override public void run() { btn_discovery.post(new Runnable() { @Override public void run() { stopDiscovery(); tv_state.setText(""); startDiscovery(); } }); } }).start(); } private String getConnectedServiceInfo(NsdServiceInfo serviceInfo) { return "service name: " + serviceInfo.getServiceName() + "\n" + "host_from_server: " + serviceInfo.getHost() + "\n" + "port from server: " + serviceInfo.getPort(); } }
  • 在測試時注意 dns_clientFIND_SERVICE_NAME 須與 dns_serverSERVICE_NAME 一致,connect 才能 work

  • dns_serverSERVICE_PORT 隨機即可。

Ref.

Listener already in use (Service Discovery)

nsdchat

android NSD服务详解

Use network service discovery (官方)

tags: 實作相關