An introduction to client / server programming

 

Introduction

One of the advantages of Java is the ease with which networking applications can be built. In this lesson you will learn the basics of Java client/server programming.

 

Overview of client/server programming

 

   

Client

Socket

<------ I/O stream ------>

Socket

 
   
      
    

Server

      
   

Client

Socket

<------ I/O stream ------>

Socket

 
   

 

 

The server

Port numbers 0 through 1023 are reserved for system use so a port number of 1024 or greater will usually be assigned to a server application.

While there are several constructors for ServerSocket, the easiest to use only requires the port number. For example, 

ServerSocket ss = new ServerSocket(7000);

creates a ServerSocket for port number 7000. Because an IOException may be thrown, the statement should be enclosed in a try block having an appropriate catch block.

while (true) {
  Socket s = ss.accept();
  new ServerThread(s).start();
}

calls the accept() method of the ServerSocket to wait for and accept a client login request. The method returns a Socket object which can be used to communicate between the two machines (the use of the Socket for communication will be covered shortly). The loop continues by launching a new thread associated with the Socket. The definition of the ServerThread class is application dependent. In this example, its constructor accepts a reference to the Socket as a parameter.

Once again, because an IOException may be thrown, the above statements should be enclosed in a try block having an appropriate catch block.

 

The client

Socket s = new Socket("as400.ferris.edu", 7000);

creates a Socket object for port 7000 on the named computer.  Because an IOException may be thrown, the statement should be enclosed in a try block having an appropriate catch block.

 

Using a Socket for communication

DataInputStream in = new DataInputStream(s.getInputStream());
DataOutputStream out = new DataOutputStream(s.getOutputStream());

create a DataInputStream object named in and a DataOutputStream named out for a previously instantiated Socket object whose reference is s. The getInputStream() and getOutputStream() methods of the Socket class return stream objects which can be "chained" to any high-level stream class. 

Because an IOException may be thrown, the statement should be enclosed in a try block having an appropriate catch block.

Once the high-level stream objects have been constructed, network I/O can be performed in exactly the same way as file I/O.

 

Maintaining a dialog

To communicate in a meaningful way, the client and the server must know who is to "talk", who is to "listen", and precisely what each other might "say" at every step of their "conversation". The rules for a how a client/server dialog might proceed must be carefully planned and programmed.

 

Sample client/server program

The following program can be run as either a client or a server. The two operating modes are combined in a single source file for convenience. To test the program:

  1. Run the program and launch the server by entering 's' at the opening prompt. A message will appear that says the program is "Running as a server".

  2. While the server is running, start the program again. This time, launch it as a client by entering 'c' at the opening prompt. A message will appear that says the program is "Connected to the server".

  3. Via the client, you may type a message that is sent to the server when you press ENTER. The server's reply will be displayed (it echoes the message back with an acknowledgement). To end client processing, press ENTER when the message has no characters (an empty string).

To see the true, multithreaded nature of the processing, launch several clients that run simultaneously. As you enter messages via the clients, see how they are received by the server. It displays each client message on the console along with the thread number of the client that sent the message.

import java.io.*;
import java.net.*;

public class App {
  public static void main(String[] args) {

    // Determine processing mode ("Server" or "Client").

    System.out.print("\n(S)erver mode or (C)lient mode: ");
    switch (Keyboard.readChar()) {

      // This case runs the program as a server.

      case 'S':
      case 's': {

        // Variable for assigning a thread number to each connection.

        int threadNo = 1;

        // Try to perform server processing. If an IOException is thrown,
        // it is processed by the catch block.

        try {

          // Display server information.

          System.out.println("Running as a server");
          System.out.println("Address is: " + InetAddress.getLocalHost());

          // Construct a server socket for port 7000.

          ServerSocket ss = new ServerSocket(7000);

          // This endless loop accepts a connection, launches a thread for
          // the connection, and increments the thread number.

          while (true) {
            Socket s = ss.accept();
            new ServerThread(s, threadNo).start();
            threadNo++;
          }
        }
        catch(IOException err) {
          System.out.println(err.getMessage());
        }
        break;
      }

      // This case runs the program as a client.

      case 'C':
      case 'c': {

        // Try to perform client processing. If an IOException is thrown,
        // it is processed by the catch block.

        try {

          // Connect to port 7000 on the server.

          Socket s = new Socket("localhost", 7000);

          // Create objects for reading and writing data streams.

          DataInputStream fromServer =
            new DataInputStream(s.getInputStream());
          DataOutputStream toServer =
            new DataOutputStream(s.getOutputStream());

          // Display connected message.

          System.out.println("Connected to the server");

          // Object reference for holding a message from the user.

          String msg;

          // This loop will read a message from the user. If it isn't an
          // empty string, the message is sent to the server and the
          // server's reply is displayed. When an empty string is entered,
          // the socket connection is closed.

          do {
            System.out.print("Message to send (null entry to exit): ");
            msg = Keyboard.readString();
            if (msg.length() > 0) {
              toServer.writeUTF(msg);
              String reply = (String) fromServer.readUTF();
              System.out.println("Server reply: " + reply);
            }
          } while (msg.length() > 0);
          s.close();
        }
        catch (IOException err) {
          System.out.println(err.getMessage());
        }
        break;
      }

      // This default case terminates the program.

      default: {
        System.out.println("Invalid mode - program terminated");
        break;
      }
    }
  }
}

// This class encapsualtes the data and processing of a server thread.

class ServerThread extends Thread {

  // Variables for maintaining the thread status and for holding the
  // thread number assigned by the server.

  boolean alive;
  private int threadNumber;

  // Object references for the socket and objects used to read from
  // the client and write to the client.

  Socket s;
  DataInputStream fromClient;
  DataOutputStream toClient;

  // This constructor initializes the thread by storing the socket
  // reference and the thread number. It then constructs objects for
  // reading from the client and writing to the client and sets the
  // thread status to indicate that the thread is alive. If an
  // IOException occurs, it is caught and the error message displayed.

  public ServerThread(Socket mySocket, int myThreadNumber) {
    s = mySocket;
    threadNumber = myThreadNumber;
    try {
      fromClient = new DataInputStream(s.getInputStream());
      toClient = new DataOutputStream(s.getOutputStream());
      System.out.println("Thread " + threadNumber + ": connected");
      alive = true;
    }
    catch(IOException err) {
      System.out.println("Thread " + threadNumber + ": " +
                         err.getMessage());
    }
  }

  // This method defines thread processing. It loops as long as the thread
  // is alive to read data from the client. As data is read, it is displayed
  // on the console and echoed back to the client to acknowledge its receipt.
  // If an EOFException is received, it means the client has closed the
  // connection and the thread status is changed. If an IOException occurs,
  // it is caught and the error message is displayed.

  public void run() {
    while (alive) {
      try {
        String data = (String) fromClient.readUTF();
        System.out.println("Thread " + threadNumber + ": \"" +
                           data + "\" (received)");
        toClient.writeUTF("\"" + data + "\" (acknowledged)");
      }
      catch (EOFException err) {
        System.out.println("Thread " + threadNumber + " closed");
        alive = false;
      }
      catch (IOException err) {
        System.out.println("Thread " + threadNumber + ": " +
                           err.getMessage());
      }
    }
  }
}

Note: To avoid the need for an external server, the "localhost" computer is used for the server application. This permits the testing of a client/server application on the same machine.

 

Looking ahead

You now have all the tools necessary to construct a sophisticated client/server application. A sample application is the topic of the next (and last) lesson.

 

Review questions

  1. True or False: Assuming that all unseen code is correct and that mySocket is a properly constructed Socket, the following constructs a high-level object capable of reading serializable objects from a network:

ObjectInputStream in = new ObjectInputStream(mySocket.getInputStream());

  1. True

  2. False

  1. Assume that all unseen code is correct, including necessary try and catch blocks. Which of the following represents part of a server application to count and display the number of times a client attempts to log into port 2345?

  1. Socket x = new Socket();
    int tryNo = 1;
    while (true) {
      x.accept(2345);
      System.out.println("Login attempt " + tryNo);
      tryNo++;
    }

  2. Socket x = new Socket(2345);
    int tryNo = 1;
    while (true) {
      x.accept();
      System.out.println("Login attempt " + tryNo);
      tryNo++;
    }

  3. ServerSocket x = new ServerSocket();
    int tryNo = 1;
    while (true) {
      x.accept(2345);
      System.out.println("Login attempt " + tryNo);
      tryNo++;
    }

  4. ServerSocket x = new ServerSocket(2345);
    int tryNo = 1;
    while (true) {
      x.accept();
      System.out.println("Login attempt " + tryNo);
      tryNo++;
    }

  5. ServerSocket x = new ServerSocket(2345);
    int tryNo = 1;
    while (true) {
      x.login();
      System.out.println("Login attempt " + tryNo);
      tryNo++;
    }

  1. True or False: A separate ServerSocket is required for each client that is connected to a server.

  1. True

  2. False

  1. Code a statement a client application could use to construct an object named x for communicating with a server application on port 2345 of a server named "shop.bigmall.com".