The Bluetooth client-server connections are implemented with the following classes:
ConnectionService
—A ConnectionService
is a runnable object that runs continuously. It listens for new
inbound connection requests from a remote server, and uses its listener
to handle the requests.
ClientConnectionHandler
—When the MIDlet is
run in the client mode, each connection is represented by a ClientConnectionHandler
.
ClientConnectionHandlerListener
—This interface
describes the callbacks of the ClientConnectionHandler
class.
ServerConnectionHandler
—When the MIDlet is
run in the server mode, each connection is represented by a ServerConnectionHandler
instance. This class is very similar
to the corresponding ClientConnectionHandler
class.
ServerConnectionHandlerListener
—This interface
describes the callbacks of the ServerConnectionHandler
class, and is very similar to the corresponding ClientConnectionHandlerListener
class.
To implement the ConnectionService
class:
Create the ConnectionService.java
class file.
Import the required classes.
import java.io.IOException; import javax.microedition.io.Connector; import javax.microedition.io.StreamConnection; import javax.microedition.io.StreamConnectionNotifier; import com.nokia.example.btsppecho.ClientForm; import com.nokia.example.btsppecho.LogScreen;
Set ConnectionService
to implement Runnable
. MIDlets use the Runnable
interface to implement classes that are executed in a thread.
public class ConnectionService implements Runnable {
Create the required
variables, the ConnectionService
class constructor,
and a method for retrieving the client URL.
private final ClientForm listener; private final String url; private StreamConnectionNotifier connectionNotifier = null; private volatile boolean aborting; public ConnectionService(String url, ClientForm listener) { this.url = url; this.listener = listener; LogScreen.log("ConnectionService: waiting to " + "accept connections on '" + url + "'\n"); // start waiting for a connection Thread thread = new Thread(this); thread.start(); } public String getClientURL() { return url; }
Create a close
method for closing the service thread.
public void close() { if (!aborting) { synchronized (this) { aborting = true; } // Ideally, one might want to give the run method's // loop a chance to abort before calling the // subsequent close, but the run loop is anyways // likely to be sitting on the acceptAndOpen // (i.e. blocked). try { connectionNotifier.close(); } catch (IOException e) { // There is nothing very useful that // we can do for this case. } } }
Create a run
method for starting the service and keeping it running.
public void run() { aborting = false; try { connectionNotifier = (StreamConnectionNotifier) Connector.open(url); // It might useful in some cases to add a service to the // 'Public Browse Group'. For example by doing something // approximately as follows: // ----------------------------------------------------- // Retrieve the service record template // LocalDevice ld = LocalDevice.getLocalDevice(); // ServiceRecord rec = ld.getRecord(connectionNotifier); // DataElement element = // new DataElement(DataElement.DATSEQ); // // The service class for PublicBrowseGroup (0x1002) // is defined in the Bluetooth Assigned Numbers document. // element.addElement(new DataElement(DataElement.UUID, // new UUID(0x1002))); // // Add to the public browse group: // rec.setAttributeValue(0x0005, element); // ----------------------------------------------------- } catch (IOException e) { // ConnectionNotFoundException is an IOException String errorMessage = "Error while starting ConnectionService: " + e.getMessage(); listener.handleError(null, errorMessage); aborting = true; } catch (SecurityException e) { String errorMessage = "SecurityException while starting ConnectionService: " + e.getMessage(); listener.handleError(null, errorMessage); aborting = true; } while (!aborting) { try { // 1. wait to accept & open a new connection StreamConnection connection = (StreamConnection) connectionNotifier.acceptAndOpen(); LogScreen.log("ConnectionService: new connection\n"); // 2. create a handler to take care of // the new connection and inform // the listener if (!aborting) { ClientConnectionHandler handler = new ClientConnectionHandler(this, connection, listener); listener.handleAcceptAndOpen(handler); } // One could consider exiting the // ConnectionService when the Client // reaches the maximum number of allowed // open connections. In that case (i.e. // when the maximum number of possible // connections is already open), the // ConnectionService will not be able // to accept any new connections and one // might possibly want to consider whether // or not the ConnectionService thread // could then be terminated. // // However, existing connections can also // be disconnected (e.g. the Server is // terminated or closes some/all of its // existing connections). In that case, // one may want to keep the // ConnectionService alive and running: // in order to accept later new connections // without the need to restart the // ConnectionService or MIDlet. // // (This MIDlet uses the latter approach.) } catch (IOException e) { if (!aborting) { String errorMessage = "IOException occurred during " + "accept and open: " + e.getMessage(); listener.handleError(null, errorMessage); } } catch (SecurityException e) { if (!aborting) { String errorMessage = "IOException occurred during " + "accept and open: " + e.getMessage(); listener.handleError(null, errorMessage); } } } } }
To implement the ClientConnectionHandler
class:
Create the ClientConnectionHandler.java
class file.
Import the required classes.
import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Hashtable; import java.util.Enumeration; import javax.microedition.io.StreamConnection;
Set ClientConnectionHandler
to implement Runnable
. MIDlets use the Runnable
interface to implement
classes that are executed in a thread.
public class ClientConnectionHandler implements Runnable {
Create the required
variables, the ClientConnectionHandler
class constructor,
and a method for retrieving the current ClientConnectionHandlerListener
.
private final static byte ZERO = (byte) '0'; private final static int LENGTH_MAX_DIGITS = 5; // this is an arbitrarily chosen value: private final static int MAX_MESSAGE_LENGTH = 65536 - LENGTH_MAX_DIGITS; private final ClientConnectionHandlerListener listener; private final Hashtable sendMessages = new Hashtable(); private StreamConnection connection; private InputStream in; private OutputStream out; private volatile boolean aborting; public ClientConnectionHandler( ConnectionService ConnectionService, StreamConnection connection, ClientConnectionHandlerListener listener) { this.connection = connection; this.listener = listener; aborting = false; in = null; out = null; // Our caller uses method 'start' to start the reader // and writer. (This allows the ConnectionService a // chance to add us to its list of handlers before // the reader and writer start running.) } ClientConnectionHandlerListener getListener() { return listener; }
Create methods for starting and closing threads.
public synchronized void start() { Thread thread = new Thread(this); thread.start(); } public void close() { if (!aborting) { synchronized (this) { aborting = true; } synchronized (sendMessages) { sendMessages.notify(); } if (out != null) { try { out.close(); synchronized (this) { out = null; } } catch (IOException e) { // there is nothing we can do: ignore it } } if (in != null) { try { in.close(); synchronized (this) { in = null; } } catch (IOException e) { // there is nothing we can do: ignore it } } if (connection != null) { try { connection.close(); synchronized (this) { connection = null; } } catch (IOException e) { // there is nothing we can do: ignore it } } } }
Create methods for receiving, reading, and sending messages.
public void queueMessageForSending(Integer id, byte[] data) { if (data.length > MAX_MESSAGE_LENGTH) { throw new IllegalArgumentException( "Message too long: limit is " + MAX_MESSAGE_LENGTH + " bytes"); } synchronized (sendMessages) { sendMessages.put(id, data); sendMessages.notify(); } } public void run() { // the reader // 1. open the streams, start the writer try { in = connection.openInputStream(); out = connection.openOutputStream(); // start the writer Writer writer = new Writer(this); Thread writeThread = new Thread(writer); writeThread.start(); listener.handleStreamsOpen(this); } catch (IOException e) { // open failed: close any connections/streams and // inform listener that the open failed close(); // also tells listener to delete handler listener.handleStreamsOpenError(this, e.getMessage()); return; } // 2. wait to receive and read messages while (!aborting) { int length = 0; try { byte[] lengthBuf = new byte[LENGTH_MAX_DIGITS]; readFully(in, lengthBuf); length = readLength(lengthBuf); byte[] temp = new byte[length]; readFully(in, temp); listener.handleReceivedMessage(this, temp); } catch (IOException e) { close(); if (length == 0) { listener.handleClose(this); } else { // we were in the middle of reading... listener.handleErrorClose(this, e.getMessage()); } } } } private static void readFully(InputStream in, byte[] buffer) throws IOException { int bytesRead = 0; while (bytesRead < buffer.length) { int count = in.read(buffer, bytesRead, buffer.length - bytesRead); if (count == -1) { throw new IOException("Input stream closed"); } bytesRead += count; } } private static int readLength(byte[] buffer) { int value = 0; for (int i = 0; i < LENGTH_MAX_DIGITS; ++i) { value *= 10; value += buffer[i] - ZERO; } return value; } private void sendMessage(OutputStream out, byte[] data) throws IOException { if (data.length > MAX_MESSAGE_LENGTH) { throw new IllegalArgumentException( "Message too long: limit is: " + MAX_MESSAGE_LENGTH + " bytes"); } byte[] buf = new byte[LENGTH_MAX_DIGITS + data.length]; writeLength(data.length, buf); System.arraycopy(data, 0, buf, LENGTH_MAX_DIGITS, data.length); out.write(buf); out.flush(); } private static void writeLength(int value, byte[] buffer) { for (int i = LENGTH_MAX_DIGITS - 1; i >= 0; --i) { buffer[i] = (byte) (ZERO + value % 10); value = value / 10; } }
The ClientConnectionHandler
class has an inner class called Writer
, which is used for handling message sending.
private class Writer implements Runnable { private final ClientConnectionHandler handler; Writer(ClientConnectionHandler handler) { this.handler = handler; } public void run() { while (!aborting) { synchronized (sendMessages) { Enumeration e = sendMessages.keys(); if (e.hasMoreElements()) { // send any pending messages Integer id = (Integer) e.nextElement(); byte[] sendData = (byte[]) sendMessages.get(id); try { sendMessage(out, sendData); // remove sent message from queue sendMessages.remove(id); // inform listener that it was sent listener.handleQueuedMessageWasSent( handler, id); } catch (IOException ex) { close(); // stop the networking thread // inform that we got an error close listener.handleErrorClose( handler, ex.getMessage()); } } if (sendMessages.isEmpty()) { try { sendMessages.wait(); } catch (InterruptedException ex) { // this can't happen in MIDP: ignore it } } } } } } }
To implement the ClientConnectionHandlerListener
class:
Create the ClientConnectionHandlerListener.java
class file.
Define the callback
methods for theClientConnectionHandler
.
public interface ClientConnectionHandlerListener { // The handler's accept and open of a new connection // has occurred, but the I/O streams are not yet open. // The I/O streams must be open to send or receive // messages. public void handleAcceptAndOpen(ClientConnectionHandler handler); // The handler's I/O streams are now open and the // handler can now be used to send and receive messages. public void handleStreamsOpen(ClientConnectionHandler handler); // Opening of the handler's I/O streams failed. The handler has // closed any connections or streams that were open. // The handler should not be used anymore, // and should be discarded. public void handleStreamsOpenError(ClientConnectionHandler handler, String errorMessage); // The handler got an inbound message. public void handleReceivedMessage( ClientConnectionHandler handler, byte[] messageBytes); // A message that had been previously queued for sending // (identified by id) by the handler, has been sent successfully. public void handleQueuedMessageWasSent( ClientConnectionHandler handler, Integer id); // The handler has closed its connections and streams. // The handler should not be used anymore, and should be discarded. // Only handlers which have previously called 'handleOpen' may // call 'handleClose', and only just once. public void handleClose(ClientConnectionHandler handler); // An error related to the handler occurred. The handler // has closed the connection, and the handler should no // longer be used. public void handleErrorClose(ClientConnectionHandler handler, String errorMessage); }