Network Service Discovery / Bonjour – Server

Before we get to the heart of this app, I will do a couple little apps that help you understand how to use NSD a little easier, Google’s example (nsdchat) is a full blown app with both the server and the client in the same one, along with fragments, classes buried in classes so the server can use the same logic as the client, it uses a Runnable Thread class which doesn’t handle reconnections (when orientations changes for instance) and for the most part it doesn’t work (flaky at best) and no documentation. You would think you would click on advertise on the server and detect and connect on the client, but that usually doesn’t work, you have to make both clients servers and then you can usually chat back and forth.

This little side app will strip out all that extra stuff and give you 2 apps (one client app and one server app).

You will start the server app and it will instantly start advertising itself.

You will start the client app and it will instantly detect the server and resolve it’s connection info and connect to the server.

Then you will be able to pass messages to the server which will display them.

Then we will dive into our first Apple code, Requires a Mac computer of some sort, which allows you to run Xcode, and you also need a Apple Developer License which you can get for free or they are $99 a year. The Apple code will allow us to act as a server and receive messages from the Android client.

This is what the MP3 player app will do also when we return to it, because all it does is send the song db number to the server which receives the number and plays the song it belongs to.

First, to use NSD you need to use a minimum of OS version of 16, JellyBean 4.1. This tutorial was tested on a JellyBean ASUS T700, KitKat Samsung S3, and a KitKat Samsung Note 3. The only issue seems to be the Note 3’s WIFI radio seems to drop connection more than it should, luckily NSD reconnects for you.

When creating the project choose Sdk version 16, Jelly Bean 4.1.

or

Edit the Manifest first to avoid errors when creating the MainActivity class.

Replace minimum with android:minSdkVersion=”16″

and add

<uses-permission android:required="true" android:name="android.permission.INTERNET"/>
 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

android.permission.INTERNET – let’s the user know that our application uses network sockets.

android.permission.ACCESS_WIFI_STATE – let’s the user know we are going to extract the WIFI IP Address.

Android NSD Server

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
 
import org.json.JSONException;
import org.json.JSONObject;
 
import android.app.Activity;
import android.content.Context;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdManager.RegistrationListener;
import android.net.nsd.NsdServiceInfo;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.widget.*;
 
public class MainActivity extends Activity {
 
 private String SERVICE_NAME = "Server Device";
 private String SERVICE_TYPE = "_letstalk._tcp.";
 private static final String REQUEST_CONNECT_CLIENT = "request-connect-client";
 private SocketServerThread socketServerThread;
 private NsdManager mNsdManager;
 
 private int SocketServerPort = 6000;
 
 private List<String> clientIPs;
 
 private static final String TAG = "NSDServer";
 
 public void showToast(final String toast){
 MainActivity.this.runOnUiThread(new Runnable(){
 public void run(){
 Toast.makeText(MainActivity.this,toast,Toast.LENGTH_LONG).show();
 }
 });
 }
 
 @Override
 public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.main);
 
 mNsdManager = (NsdManager) getSystemService(Context.NSD_SERVICE);
 registerService(9000);
 
 clientIPs = new ArrayList<String>();
 socketServerThread = new SocketServerThread();
 socketServerThread.start();
 }
 
 public void registerService(int port) {
 NsdServiceInfo serviceInfo = new NsdServiceInfo();
 serviceInfo.setServiceName(SERVICE_NAME);
 serviceInfo.setServiceType(SERVICE_TYPE);
 serviceInfo.setPort(port);
 
 mNsdManager.registerService(serviceInfo,NsdManager.PROTOCOL_DNS_SD,mRegistrationListener);
 }
 
 RegistrationListener mRegistrationListener = new NsdManager.RegistrationListener() {
 
 @Override
 public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {
 String mServiceName = NsdServiceInfo.getServiceName();
 SERVICE_NAME = mServiceName;
 Log.d(TAG, "Registered name : " + mServiceName);
 }
 
 @Override
 public void onRegistrationFailed(NsdServiceInfo serviceInfo,
 int errorCode) {
 // Registration failed! Put debugging code here to determine
 // why.
 }
 
 @Override
 public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
 // Service has been unregistered. This only happens when you
 // call
 // NsdManager.unregisterService() and pass in this listener.
 Log.d(TAG,
 "Service Unregistered : " + serviceInfo.getServiceName());
 }
 
 @Override
 public void onUnregistrationFailed(NsdServiceInfo serviceInfo,
 int errorCode) {
 // Unregistration failed. Put debugging code here to determine
 // why.
 }
 };
 
 private class SocketServerThread extends Thread {
 
 @Override
 public void run() {
 
 Socket socket = null;
 ServerSocket serverSocket = null;
 DataInputStream dataInputStream = null;
 DataOutputStream dataOutputStream = null;
 
 try { 
 Log.i(TAG, "Creating server socket"); 
 serverSocket = new ServerSocket(SocketServerPort);
 
 while (true) {
 socket = serverSocket.accept();
 dataInputStream = new DataInputStream(
 socket.getInputStream());
 dataOutputStream = new DataOutputStream(
 socket.getOutputStream());
 
 String messageFromClient, messageToClient, request;
 
 //If no message sent from client, this code will block the Thread
 messageFromClient = dataInputStream.readUTF();
 
 final JSONObject jsondata;
 
 try {
 jsondata = new JSONObject(messageFromClient);
 request = jsondata.getString("request");
 
 if (request.equals(REQUEST_CONNECT_CLIENT)) {
 String clientIPAddress = jsondata.getString("ipAddress");
 
 // Add client IP to a list
 clientIPs.add(clientIPAddress);
 showToast("Accepted");
 
 messageToClient = "Connection Accepted";
 
 
// Important command makes client able to send message
 dataOutputStream.writeUTF(messageToClient);
// ****** Paste here Bonus 1
 
// ****** Paste here Bonus 1
 } else {
 // There might be other queries, but as of now nothing.
 dataOutputStream.flush();
 }
 
 } catch (JSONException e) {
 e.printStackTrace();
 Log.e(TAG, "Unable to get request");
 dataOutputStream.flush();
 }
 }
 
 } catch (IOException e) {
 e.printStackTrace();
 } finally {
 if (socket != null) {
 try {
 socket.close();
 } catch (IOException e) {
 e.printStackTrace();
 }
 } 
 
 if (dataInputStream != null) {
 try {
 dataInputStream.close();
 } catch (IOException e) {
 e.printStackTrace();
 }
 }
 
 if (dataOutputStream != null) {
 try {
 dataOutputStream.close();
 } catch (IOException e) {
 e.printStackTrace();
 }
 }
 }
 
 } 
 
 }
 
 protected void onPuase() {
 if (mNsdManager != null) {
 mNsdManager.unregisterService(mRegistrationListener);
 }
 super.onPause();
 }
 
 @Override
 protected void onResume() {
 super.onResume();
 if (mNsdManager != null) {
 registerService(9000);
 }
 
 }
 
 @Override
 protected void onDestroy() {
 if (mNsdManager != null) {
 mNsdManager.unregisterService(mRegistrationListener);
 }
 super.onDestroy();
 }
 
}

SERVICE_NAME is a constant that we use to set the device name. If more than one device has the same service name, the 2nd device has a (1) added to it and the 3rd would have (2) added to it and so on. Not sure if this is what made the nsdChat version so flaky so I don’t use it, I give the server a name and the client a name, and use that.

SERVICE_TYPE is a custom name you make up (that must conform to a standard). This is the name of your app on the network, it doesn’t have to be the same name as your app, just every device that you want to see each other must use the same one. The service type specifies which protocol and transport layer the application uses. The syntax is “_[protocol]._[transportlayer].” You can name the protocol anything you want but leave the transportlayer the way it is.

Note: If you plan on publishing an app to the app store that uses NSD you should register your protocol to the International Assigned Numbers Authority (IANA). They manage a centralized, authoritative list of service types used by service discovery protocols such as NSD and Bonjour. If you intend to use a new service type, you should reserve it by filling out the IANA Ports and Service registration form.

REQUEST_CONNECT_CLIENT is a static constant we use to detect what the client wants to do. At first this will be the only option, but later we will add a display message option. Static means that you can access the constant value from other classes without having to instantiate the class. A class is like blue prints on how to build a house, if you use the blue prints to instantiate the house, you build the house. A static value is like a equation on how to figure square footage that happens to also be written on the blue prints. Just because the equation is written on the blue prints it doesn’t mean you have to build the house to use them.

Now we layout our tools we want to use.

SocketServerThread is a custom class that we use to add functionality to the Thread class. The functionality that we add listen’s for incoming connections and receive’s data from the client.

Threads allow us to perform work simultaneously along with the main Thread. The main Thread is what the app display runs on and what Android watches to detect misbehaving apps that it needs to shut down to keep the phone performing in a user friendly way. The server will be waiting for a connection to be requested and established and it can not do this on the main Thread or Android will shut it down.

NsdManager is what we use to make the server discoverable by other devices. The client devices use NsdManager to discover us and then resolve (connect) to us.

List clientIPs is a list of Strings that will hold the IP Addresses of all the connected clients. This is not required to make the app work, and will not be used by me, but might be helpful to you if you need to know that information.

A TAG file is used for logging, it helps catch your eye when looking at logcat while your app is running to make sure it is making it through all steps of your program. This was one of the few concepts that I had to use logging for, especially with the nsdchat app, but that was before I found the showToast method on stackOverflow, which I show you next.

public void showToast(final String toast) {
    MainActivity.this.runOnUiThread(new Runnable() {
        public void run() {
            Toast.makeText(MainActivity.this, "Accepted", Toast.LENGTH_LONG).show();
        }
    });
}

This is one of the best tools ever! Especially if you are debugging on your phone. This allows you to Toast whatever you want to the screen from anywhere in the app. If you remember earlier when I was discussing Context I said you had to have access to the main screen to be able to see a Toast, and you can’t always get Context. But with this tool you don’t need to. You can pass any string you like to this method() but for this app I just Toast a static message of Accepted.

Now with our tools laid out let’s walk through the logic.

We display our screen with setContentView(R.layout.main); – Displayed at the bottom.

We create an instance of NsdManager called mNsdManager to use to make our server discoverable.

We run a custom method registerService().

This method initializes an instance of NsdServiceInfo that we use to assign our service name, service type, and service port.

We then use NsdManager to register our service (server) on the network using our NSDServiceInfo (which will be passed to the client during discovery and resolve, we define what type of protocol we are using (for NSD you use NsdManager.PROTOCOL_DNS_SD) and where to go after the service is successfully registered (a interface we create for callbacks called mRegistrationListener).

Next we define, initialize and implement the RegistrationListener interface. We use this to tell if we successfully registered the service. Since it is an interface we have methods() we must implement (@Override) to receive service status change call backs.

public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {

Is executed when a the service is successfully registered. We use the NsdServiceInfo we receive to Log the service name that was registered.(This would be helpful if you were registering multiple different services).

public void onRegistrationFailed(NsdServiceInfo serviceInfo,
int errorCode) {

Is executed when the registration fails, you could log the error if you need to, but we don’t in this example.

public void onServiceUnregistered(NsdServiceInfo serviceInfo) {

This is run if the client is turned off.

public void onUnregistrationFailed(NsdServiceInfo serviceInfo,
int errorCode) {

This is run if registration fails.

Next we define our custom background Thread that we use to accept connections, receive and send data.

We initialize our Socket, used to communicate with the clients, and set it’s value to null.

We initialize our ServerSocket, used to accept connections, and set it to null.

We initialize our DataInputStream, that receives incoming data, and set it to null.

We initialize our DataOutputStream, used to send data to clients, and set it to null.

We use a try and catch because a port may not be available or configured wrong.

We then create our ServerSocket that we use to listen for incoming connections. Which is a pre-defined integer variable (SocketServerPort = 6000).

while(true){ – is known as an infinite loop, while remains true as long as the app is running.

Now comes the main reason why we use a Thread.

socket = serverSocket.accept();

This command sits and waits till a connection comes in, if this were on the main thread it would cause the program to time out and crash. It’s called a blocking method.

When a connection does come in you can not use the same socket to talk with the client, because the ServerSocket is always waiting and listening for just incoming calls, so when a new connection is made a new socket is created and assigned to the socket variable

We can then use this socket to get a InputStream and OutputStream for the socket.

Then we wait for the client to send some data.

messageFromClient = dataInputStream.readUTF();

This is another blocking method that doesn’t allow the code to continue until data is received.

The data that the client sends us in this app is in a JSONObject format which is similar to a HashMap format, or a string value pair. It is essentially a variable name with a value. You can check for the existence of the variable, and if it exists get the value. The variable is the name for the value.

Since the client will be sending a JSONObject we create one to store the data passed in.

Since the data passed in might not end up being a JSONObject we surround it with a try and catch to debug issues if there are any.

We initialize the JSONObject by putting the data passed in from the client in it.

We then get the value (which is a String) out of the variable request.

If the value stored in request equals the constant stored in REQUEST_CONNECT_CLIENT, which is “request-connect-client”

Then we get the value stored in ipAddress (which is a String).

Then we take that value and add it to our List of client IP Adresses (which we do nothing with).

We then display a toast so we can tell the server received our message from the client.(to help with debugging)

We then create a message to send back to the client “Connection Accepted”.

We then send the message using writeUTF to send data out the OutputStream using the socket.

dataOutputStream.writeUTF(messageToClient);

if (request.equals(REQUEST_CONNECT_CLIENT)) is FALSE.

dataOutputStream.flush();

Which removes all data from the OutputStream, this will be useful for our MP3 player because we will be listening for a certain amount of data (20 characters) and if there are data remnants left in the pipe this could really mess up our data flow.

First we catch our last try statement which tries to pull data out of the JSONObject so we catch a JSONException. If there is an JSONException we do a printStackTrace() and flush any data that might have made it in the pipe.

Next we catch a IOException in case we have issues creating the Socket, InputStream, or OutputStream.

After making it through the Thread and only after making it through the Thread do we try to close the socket, close the OutputStream, and close the InputStream. (if they have been used( i.e. !=null))

Finally if another application is opened over this app.

protected void onPuase() {

If NsdManager is being used, we can stop advertising it’s existence.

mNsdManager.unregisterService(mRegistrationListener);

If the app is re-opened.

protected void onResume() {

and NsdManager is still available we can re-advertise our service.

registerService(9000);

Or if our app is completely removed from memory by the system, we can stop advertising it’s existence.

mNsdManager.unregisterService(mRegistrationListener);

That’s everything you need all in one file.

Except for the layout/main.xml file.

"http://schemas.android.com/apk/res/android"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:gravity="center"
 android:orientation="vertical" >

For this example we are connecting a Android client to a Android server using Network Service Discovery. We have set up the server. In my next post we will set up the client.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s