mDNS 與 DNS-SD 實作
===
本實作主要參考 [nsdchat](https://android.googlesource.com/platform/development/+/master/samples/training/NsdChat/src/com/example/android/nsdchat),出處應來自 Android Developers 官方,但據下方文章作者提及官方好像已經移除且也找不到了,這是舊的連結,不過官方依然有 sample code 只是比較簡化就是。
本人參考後無法正常運行,且範例是沒有區別 server 與 client ,在 trace 過程中有點難理解,故自行簡化重新修改成兩種版本:`dns_server`與`dns_client`,可用兩台不同裝置分別安裝來測試看看:
兩支不同專案實作時,記得在 `AndroidManifest.xml` 加上
`<uses-permission android:name="android.permission.INTERNET"/>`
### dns_server

* `activity_main.xml`
```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`
```java=
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=
<?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`
```java=
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_client** 的 `FIND_SERVICE_NAME` 須與 **dns_server** 的 `SERVICE_NAME` 一致,connect 才能 work
* **dns_server** 的 `SERVICE_PORT` 隨機即可。
## Ref.
[Listener already in use (Service Discovery)](https://stackoverflow.com/a/35108713)
[nsdchat](https://android.googlesource.com/platform/development/+/master/samples/training/NsdChat/src/com/example/android/nsdchat)
[android NSD服务详解](https://blog.csdn.net/wenzhi20102321/article/details/79575836)
[Use network service discovery](https://developer.android.com/training/connect-devices-wirelessly/nsd#java) (官方)
###### tags: `實作相關`