Home : Course Map : Chapter 15 :
The Data Server
JavaTech
Course Map
Chapter 15

Client/ServerDesign
Processing Steps
Data Server
Run Data Server
    Demo 1
Data Client
DataClient Applet
    Demo 2
Client/Server Sim
SimServer
    Demo 3
SimClient
Run SimClient
    Demo 4
Exercises

     About JavaTech
     Codes List
     Exercises
     Feedback
     References
     Resources
     Tips
     Topic Index
     Course Guide
     What's New

Our DataServer application sends simulated data to a DataClient. An image of the DataServer application in action is given on the next page. Also, complete code listings for the DataServer and DataWorker classes are given there. Here we discuss key parts of the two classes.

As we outlined in the previous page, the server follows a simple protocol with the client in which each knows when to send a message and when to wait for a message. (And when to send or receive numerical data.) The server initially carries out a simple log-in procedure, which here just means a request for a user name. One could easily expand this to include a password exchange as well.

Once the login process is finished, the server hands off the client to an instance of the Thread subclass DataWorker. The server also must do some bookkeeping to keep track of multiple clients. This includes keeping track of current clients, resetting the arrays when a client breaks off, and sending a warning message when new clients want to make a connection but the maximum number of clients has been reached.

DataServer Class

Firstly, we note that to let the user see what is happening, the graphical interface for DataServer includes a text area to display the status of the server and to show messages indicating the various actions taken as connections to the clients occur and services are provided. The utility method println() sends messages to the text area:

public void println (String str) {
  fTextArea.append (str + "\n");
  repaint ();
}

The "Start" button puts the server program into a server state by starting a thread with a loop in the run() method. Clients requesting connections are detected via a ServerSocket and a new instance of DataWorker is created to handle each new client. When a connection is made, the program opens I/O streams to and from the client.

... The run() method in class DataServer ...

  /** Create a ServerSocket and loop waiting for clients. **/
  public void run () {

    // The server_socket is used to make connections to
    // DataClients at this port number
    try {
        fServerSocket = new ServerSocket (fDataServerPort);
    }
    catch (IOException e) {
        println ("Error in server socket");
        return;
    }
    println ("Waiting for users...");
    // Loop here to grab clients
    while (fKeepServing) {
      try {
          // accept () blocks until a connection is made
          Socket socket = fServerSocket.accept ();

          // Do the setup this socket and then loop
          // back around to wait for the next DataClient.
          DataWorker worker = new DataWorker (this, socket);
          worker.start ();
      }
      catch (IOException ioe) {
          println ("IOException: <" + ioe + ">");
          break;
      }
      catch  (Exception e)  {
          println ("Exception: <" + e + ">");
          break;
      }
    }
  } // run

 

DataServer uses a Vector to keep a list of all the clients, and a limit on the number of clients is set with the fMaxClients variable. A DataWorker calls back to the clientPermit() method, shown below, to determine if it can join the list. If so, then it invokes clientConnected(), which adds the client to the list.

When the client disconnects, the DataWorker invokes the server's clientDisconnected() method to remove itself from the worker list. These methods are synchronized to avoid any interference if two or more threads are connecting/disconnecting at the same time.

  ... continue in class DataServer ...

 /**
    * When a DataWorker makes the connection, it checks to see if
    * there is room on the server for it.
    * We synchronize the method to avoid any problems with multiple
    * clients interfering with each other.
   **/
  public synchronized boolean clientPermit () {
    if (fWorkerList.size () < fMaxClients__)
        return true;
    else
        return false;
  }

  /**
    * A DataWorker will set up the connection with the client. If it
    * decides that the conditions are OK, then it will invoke this
    * method so that the parent server will add the worker to its
    * list.
    * We synchronize the method to avoid any problems with multiple
    * clients interfering with each other.
   **/
  public synchronized void clientConnected (DataWorker worker) {
    fWorkerList.add (worker);
    fClientCounter__++;
  }

  /**
    * When a client disconnects, the DataWorker object will
    * call back to this method to remove itself from the list
    * of workers.
    *
    * We synchronize the method to avoid any problems with multiple
    * clients interfering with each other.
   **/
  public synchronized void clientDisconnected (String user,
                                               DataWorker worker) {
    println ("Client: "+user+" disconneced");
    fWorkerList.remove (worker);
    fClientCounter__--;
  }
 ...

 

The DataServer hands off the client socket to a DataWorker, which then begins its job of communicating with the client and providing the requested services.

DataWorker Class

The job of the DataWorker is to tend to the needs of the client. The worker waits for a request for data from the client. When the request arrives, the worker tells the client how many data values will be included in the data set. It then generates a set integer values, each according to a Gaussian distribution (different data channels use different Gaussian widths and offsets) and sends the data set to the client.

As shown in the following code snippet, the first act by the run() method is to invoke the serviceSetup() method. This method sets up the streams for I/O with the client. The PrintWriter and BufferedReader wrappers are used to send and receive text to and from the client. A DataOutputStream wrapper is used to send numerical values. The read/write methods for these streams are put into some utility methods discussed later.

If the maximum number of clients has been reached, the worker sends a warning message to its client and breaks off the connection. That worker thread itself then signs off from the server and dies.

If there is space for the new client, the serviceSetup() method performs a simple log-in procedure with the client that consists of sending the string "Username:" to the client and waiting for a string in return.

  ... The run() and serviceSetup() methods in class DataWorker ...

  /**  Send data to the client. **/
  public void run  () {

    // If setup fails, end thread processing.
    if  (!serviceSetup ()) return;

     ...

  } // run

  /**
    * Set up the connection to the client. This requires obtaining the
    * IO streams, carrying out the login prototcol, and then starting
    * a DataWorker thread to tend to the client.
    *
    * The bookkeeping code is a bit messy because we check both reads
    * and writes for errors in case the connection breaks down.
    *
    * The reads catch their own IOExceptions and return a null, while
    * string writes use a PrintWriter that doesn't throw IOException. So
    * we use the checkError () method and throw it ourselvefDataServer.
   **/
  public boolean serviceSetup ()  {

    fDataServer.println ("Client setup...");

    // First get the in/out streams from the socket to the client
    try{
      fNetInputStream  = fSocket.getInputStream ();
      fNetOutputStream = fSocket.getOutputStream ();
    }
    catch  (IOException e){
      fDataServer.println ("Unable to get input/output streams");
      return false;
    }

    // Create a PrintWriter class for sending text to the client.
    // The writeNetOutputLine method will use this class.
    try{

        fPrintWriter = new PrintWriter (
           new OutputStreamWriter (fNetOutputStream, "8859_1"), true );
    }
    catch (Exception e) {
        fDataServer.println ("Fails to open PrintWriter to client!");
        return false;
    }

    // Check if the server has room for this client.
    // If not, then send a message to this client to tell it
    // the bad news.
    if  ( !fDataServer.clientPermit () ) {
      try{

        String msg= "Sorry, We've reached maximum of clients";
        writeNetOutputLine (msg);
        fDataServer.println (msg);
        return false;

      }
      catch  (IOException e){
        fDataServer.println ("Connection fails during login");
        return false;
      }
    }

    // Get a DataInputStream wrapper so we can use its
    // readLine () methods.
    fNetInputReader  =
    
new BufferedReader (new InputStreamReader (fNetInputStream));

    // Do a simple login protocol. Send a request for the users name.
    // Note a password check could be added here.
    try{
      writeNetOutputLine ( "Username: ");
    }
    catch (IOException e){
      fDataServer.println ("Connection fails during login");
      return false;
    }

    // Read the user name.
    fUser = readNetInputLine ();
    if  (fUser == null ) {
        fDataServer.println ("Connection fails during login");
        return false;
    }

     // Send a message that the login is OK.
    try{
      writeNetOutputLine ("Login successful");
      fDataServer.println ("Login successful for " + fUser);
    } catch  (IOException e){
      fDataServer.println ("Connection fails during login for "
                            + fUser);
      return false;
    }
    fDataServer.println (fUser + " connected! ");
    fDataServer.println (fSocket.toString ());


    // The login is successful so now create a DataWorker to
    // service this client. Pass it an ID number
    fDataServer.clientConnected (this);

    // Get a data output stream for writing numerical data to the client
    fDataOutputStream = new DataOutputStream (fNetOutputStream);

    return true;
  } // serviceSetup

 ...

 

If the log-in procedure is successful, then a call back to the server informs it that the connection for this client is successful and it should be added to the client list.

The stream methods to send or receive strings and data involve several lines of code since they can throw exceptions. Also, to insure that data does not get stuck in a buffer, a flush() method is invoked. So rather than repeating all of this code for each read or writer operation, the DataWorker uses the following utility methods.

  ... Continue in DataWorker ...

  /** Utility method to read a whole text line.**/
  String readNetInputLine ()  {
    try {
      return fNetInputReader.readLine ();
    }
    catch  (IOException e){
      return null;
    }
  } // readNetInputLine

  /**
    * Output is wrapped with a PrintWriter, which doesn't throw
    * IOException. So we invoke the checkError() method and then
    * throw an exception if it detects an error.
   **/
  void writeNetOutputLine (String string)
                  throws IOException {

    fPrintWriter.println (string);
    if  ( fPrintWriter.checkError ()) throw  (new IOException ());

    fPrintWriter.flush ();
    if  ( fPrintWriter.checkError ()) throw  (new IOException ());
  } // writeNetOutputLine

  /** Utility to write integer values to the output stream. **/
  void writeNetOutputInt (int i)
      throws IOException {
    fDataOutputStream.writeInt (i);
    fDataOutputStream.flush ();
  } // writeNetOutputInt

  /** Utility to write float values to the output stream.**/
  void writeNetOutputFloat (float f)
      throws IOException {
    fDataOutputStream.writeFloat (f);
    fDataOutputStream.flush ();
  } // writeNetOutputFloat

 ...

 

Once the connection with the client has been established, the run() method (shown below) enters a loop where it waits for a request from the client, responds to the request, and then waits for the next request. For the simple situation here, any string received from the client is assumed to be a request for data.

The worker first tells the client how many data values are included the data set. It then generates an array of integer values, where the generation for each array element comes from a random Gaussian distribution with different widths and offsets. This data array is then sent to the client just as if it were a set of data readings from, say, an experimental apparatus.

  ... the run() method in DataWorker ...

  /**  Send data to the client. **/
  public void run  () {

    // If setup fails, end thread processing.
    if  (!serviceSetup ()) return;

    fDataServer.println (
      "Client connection and login OK - Begin service...");

    // Lower tpriority to give main parent and
    // other threads some processor time.
    setPriority (MIN_PRIORITY);

    String client_last_msg = "";

    // Begin the loop for communicating with the client.
    while  (fKeepRunning){

      // Read a request from the DataClient
      String client_msg = readNetInputLine ();
      if (client_msg == null ) break;

      // Only print message if it changes. Avoids printing same
      // message for each data set.
      if  ( !client_msg.equals (client_last_msg))
          fDataServer.println ("Message from " + fUser + ": "
                                + client_msg);
      client_last_msg = client_msg;

      // Could interpret the request and do something accordingly
      // but here we will just send a set of data values.


      // Send the number of data values.
      try {
        writeNetOutputInt (DataServer.fNumDataVals__);
      }
      catch  (IOException e) {
        break;
      }

      // Creat dummy data values and send them to the DataClient.
      for (int i=0; i< DataServer.fNumDataVals__; i++){

        // Select that range of Gaussian widths for the data
        // values for each channel of the data set. Add an offset
        // to get most negative values above zero.
        int i_std_dev = i%6;
        double dat = 3.0*fStdDev[i_std_dev] +
                     fStdDev[i_std_dev] * fRan.nextGaussian ();
        if  (dat < 0.0) dat = 0.0;

        // Pass only integer values;
        int idat =  (int) dat;
        try {
          writeNetOutputInt (idat);
        }
        catch  (IOException e) {
          break;
        }
      }
    }

    // Send message back to the text area in the frame.
    fDataServer.println (fUser + " has disconnected.");

    // Do any other tasks for ending the worker.
    signOff ();

  } // run

 

The DataWorker could be modified to obtain real data in various ways. For example, a sensor might generate a file that the worker could read and send to the client. In Chapter 23 we will discuss how Java programs can communicate with devices via the serial ports. The worker might obtain data from a device that way and then send it to the client.

 

Last update: Dec. 11, 2004

  
  Part I Part II Part III
Java Core 1  2  3  4  5  6  7  8  9  10  11  12 13 14 15 16 17
18 19 20
21
22 23 24
Supplements

1  2  3  4  5  6  7  8  9  10  11  12

Tech 1  2  3  4  5  6  7  8  9  10  11  12
Physics 1  2  3  4  5  6  7  8  9  10  11  12

Java is a trademark of Sun Microsystems, Inc.