Client/Server Programming -- Week 2

Introduction

This week we'll talk about using sockets for communicating between client and server.

Note that the links from this page are to handouts that will be distributed the night of class.  Only print out those handouts if you could not attend class.

Main topics this week:

Sockets
Types of Connections
InetAddress
Client Socket
Client Example
PrintWriter and buffering
Server Socket
Simple Server Example
Multiple Clients
Understanding a Socket Program
Beyond Sockets
Assignment 2-1
Next Week

Sockets

A socket is one end of a raw TCP/IP connection from one machine to another machine.   The connection is called "raw", because the only thing you can do is to send bytes from one machine to another.  A socket connection itself has no support for sending more complex forms of data, such as integers, doubles, or strings. 

The process of preparing more complex data for transmission over a socket is called marshalling the data.  You might marshall an integer by breaking it up into four separate bytes for transmission.  The process of taking the bytes and recreating the more complex data type on the receiving end is called unmarshalling the data.  A socket connection does not do any marshalling or unmarshalling of data for you. 

Sockets are the most flexible way of communicating between a client and a server, because you can send whatever you want over the socket in whatever way you want.  But sockets also require the most work, since you must do it all yourself.  Note that Java does have some classes that help with simple marshalling and unmarshalling of data over sockets, but these classes are not part of the socket itself.

A socket is identified by a machine and a port number.  The port number allows the machine to figure out which application to run to handle the connection.  For example, a socket connection to port 80 on most machines will connect to a web server, if one is running on the machine.  Port numbers are represented by integers from 1 to 65535.  On most machines, the first 255 or so port numbers are taken up by well-known applications, such as web servers, email servers, etc.

Types of Connections

A socket connection may be one of two types: connection-oriented, or connectionless.  

A connectionless socket simply sends data to the target machine without worrying whether the target machine receives the data or not.   Data might very well not reach the target machine, but the sender will never know this. 

A connection-oriented socket will communicate with the target machine upon being opened, to ensure that the host is available.  Additionally, the target machine will acknowledge everything sent to it, so the sender will know if any data did not reach the target.  Since the target must acknowledge every piece of data received, the amount of network traffic is higher than with connectionless sockets.  However, this provides reliability to the connection, and is the type of connection we will use in this class. 

(diagram each in a sequence diagram showing data transmitted)

InetAddress

The InetAddress class represents an Internet address (e.g. ip address), and is used by both the client and server side socket classes to identify a target machine on the network.

There are no public constructors in the InetAddress class.  Instead, to create an instance you call one of several static methods on the class.

InetAddress.getLocalHost () -- This returns an InetAddress instance that represents the machine the program is running on (e.g. the "local host"). 

InetAddress.getByName (String hostName) -- This returns an InetAddress instance that represents the given host name.  For example, "InetAddress.getByName ("einstein.franklin.edu")". 

InetAddress.getByAddress (byte [] ipAddress) -- This returns an InetAddress instance based on an ip address.  The ipAddress array should be four elements long, one element for each portion of the ip address. 

Each of these static methods returns an instance of InetAddress that points to the given machine.  They can all throw an UnknownHostException if the given host cannot be found.  For example, if you call getByName and pass in the name of a machine that does not exist, the UnknownHostException will be thrown.  This exception may also be thrown if your computer is having trouble communicating with the domain name servers, so cannot convert the name to an ip address.

Client Socket

In Java, the client side of a socket connection uses the Socket class.  This class provides for sending data to the server via a byte-oriented OutputStream, and reading data from the server via a byte-oriented InputStream.

You create a Socket instance by giving it a host and a port.  The host may be specified as a host name, or as an InetAddress.   The more useful constructors are:

Socket (InetAddress server, int port)

Socket (String hostName, int port)

Both constructors create an instance of Socket that point to the given host and port number.  Each will throw an IOException if the socket connection cannot be made (for example, the target machine is not connected to the network, or is turned off).   Other exceptions may also be thrown...look at the Java API guide for details.

Once you have created a Socket instance successfully, you are now ready to read and write on the socket.  There are two methods that you will use for this:

InputStream getInputStream () -- This returns an InputStream that is connected to the socket.  Remember that an InputStream will read byte-oriented data.   Typically you may want to read String oriented data, which would mean wrapping this InputStream in an InputStreamReader inside a BufferedReader...the BufferedReader would provide ummarshalling to convert the bytes into strings.  We'll see an example of this later. 

OutputStream getOutputStream () -- This returns an OutputStream connected to the socket.  An OutputStream is also byte-oriented, so you will usually want to wrap it inside a PrintWriter, which will provide marshalling of more complex types into bytes.

Using the input and output streams you can read and write data on the socket.   When you are finished, you must release the resources used by the socket by calling the close () method on the socket. 

Client Example

Here is an example client program that uses sockets to connect to a server.  This client simply allows the user to type at the keyboard, and sends everything the user types to the server.  Whatever the server sends back is then displayed to the user. 

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

public class Client
{
  public static void main(String[] args) throws IOException
  {
    Socket server = null;
    PrintWriter serverOut = null;
    BufferedReader serverIn = null;

    try
    {
      server = new Socket("localhost", 4444);
      serverOut = new PrintWriter(server.getOutputStream(), true);
      serverIn = new BufferedReader(new InputStreamReader(server.getInputStream()));
    }
    catch (UnknownHostException e)
    {
      System.err.println("Can't find localhost.");
      return;
    }
    catch (IOException e)
    {
      System.err.println("IO Error on connection to localhost. Is server running?");
      return;
    }

    BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
    String userInput;

    do
    {
      userInput = stdIn.readLine();
      serverOut.println(userInput);
      System.out.println ("Server: " + serverIn.readLine ());
    }
    while (! userInput.equals ("Bye"));

    serverOut.close();
    serverIn.close();
    server.close();
  }
}

(diagram the client, the server, the keyboard, and the connections between each using the classes in the code)

PrintWriter and buffering

You will use the PrintWriter class to marshall data to the socket, as we saw in the client example.  Note that the PrintWriter class will, by default, buffer output until it feels the need to send the output on to the destination OutputStream.  For sockets, this means that your information you send may not actually get sent right away.  You can disable this buffering when you create the PrintWriter by passing in a second parameter that should be true.   For example:

PrintWriter out = new PrintWriter (socket.getOutputStream (), true);

This ensures that anything you send using the PrintWriter is sent immediately, which is usually what you want for client/server applications.

Server Socket

In Java, the server side of a socket connection is represented by the ServerSocket class.  You create an instance of ServerSocket when the server is ready to listen for client connections.  The most useful constructors are:

ServerSocket (int port) -- This creates a ServerSocket instance and binds it to the specified port.  Client connections made to that port will be connected to this ServerSocket instance.  A maximum of 50 client connections can be waiting for processing.

ServerSocket (int port, int backlog) -- This creates a ServerSocket instance and binds it to the specified port.  The value of the backlog parameter is how many client connections can be waiting for processing. 

Both constructors may throw an IOException is there is a problem opening the socket.  

Once you have a ServerSocket instance created and listening to a port, you need to use the accept () method to tell the ServerSocket to wait for a client connection.   The accept method will wait until a client attempts to connect.  At that point, the accept method will return a Socket instance that represents that client connection.  You use the Socket instance in the same way as the client, getting the InputStream and OutputStream and using them to read and write data on the socket. 

If you want to use multithreading to enable multiple client connections to be handled simultaneously, you would start a separate thread after the accept method returns a Socket.  That way the main loop could go back to the accept call and wait for another connection while the separate thread would handle the client's request.

Simple Server Example

Here's a simple server example that works with the client example above:

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

public class Server
{
    public static void main(String[] args) throws IOException
    {
        ServerSocket serverSocket = null;
        try
        {
            serverSocket = new ServerSocket(4444);
        }
        catch (IOException e)
        {
            System.err.println("Could not listen on port: 4444.");
            System.exit(-1);
        }

    Socket client = null;   
    BufferedReader clientIn = null;
    PrintWriter clientOut = null;
    String input = null;

    try
    {
      client = serverSocket.accept ();
      clientIn = new BufferedReader (new InputStreamReader (client.getInputStream ()));
      clientOut = new PrintWriter (client.getOutputStream (), true);
    }
    catch (Exception e)
    {
      e.printStackTrace ();
    }

    do
    {
      try
      {
        input = clientIn.readLine ();
        clientOut.println (input.toUpperCase ());
      } 
      catch (Exception e)
      {
        e.printStackTrace ();
      }
    }
    while (! input.equals ("Bye"));

    try
    {
      clientIn.close ();
      clientOut.close ();
      client.close ();
    }
    catch (Exception e)
    {
      e.printStackTrace ();
    }

        serverSocket.close();
    }

}

This server simply waits for a client to connect, and then reads data from the client.   Whatever the client sends to the server is sent back to the client after being converted to upper case. 

This server can only handle one client at a time, which is quite limiting for a server.   Let's look at a more complex example that can handle multiple clients.

Multiple Clients

To handle multiple clients, the server must be written so that it starts a separate thread for each client connection.  This allows the main server thread to continue waiting for new client connections while the separate threads processes individual clients.

Here's an example server that can handle mutliple clients:

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

public class Server
{
    public static void main(String[] args) throws IOException
    {
        ServerSocket serverSocket = null;
        boolean listening = true;

        try
        {
            serverSocket = new ServerSocket(4444);
        }
        catch (IOException e)
        {
            System.err.println("Could not listen on port: 4444.");
            System.exit(-1);
        }

        while (listening)
            new Thread (new ClientHandler (serverSocket.accept()))).start();

        serverSocket.close();
    }
}

In this server code, the class ClientHandler is simply a class that implements Runnable and does something with the socket connection in the run method.  As written, this server could be used for any purpose, since the details of what the server actually does are hidden inside the ClientHandler class.

Here's a simple example of ClientHandler that will echo back to the client everything the client sends, until the client sends the text "Bye" (this matches with our previous client example):

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

class ClientHandler implements Runnable
{
  private Socket client;

  public ClientHandler (Socket c)
  {
    client = c;
  }

  public void run ()
  {
    BufferedReader clientIn = null;
    PrintWriter clientOut = null;
    String input = null;

    try
    {
      clientIn = new BufferedReader (new InputStreamReader (client.getInputStream ()));
      clientOut = new PrintWriter (client.getOutputStream (), true);
    }
    catch (Exception e)
    {
      e.printStackTrace ();
    }

    do
    {
      try
      {
        input = clientIn.readLine ();
        clientOut.println (input);
      } 
      catch (Exception e)
      {
        e.printStackTrace ();
      }
    }
    while (! input.equals ("Bye"));

    try
    {
      clientIn.close ();
      clientOut.close ();
      client.close ();
    }
    catch (Exception e)
    {
      e.printStackTrace ();
    }
  }
}

Understanding a Socket Program

The biggest problem many people have with socket programming is keeping track of what the client sends and what the server does with it.  I'd like to introduce a diagramming notation that may help.  You should have already seen sequence diagrams in comp313.  This diagram is like a sequence diagram, but we're going to add in some other bits to it. 

Start with a sequence diagram with two objects, Client and Server.  We're using one client because our example program has only one client.  If you needed more clients you'd have a box for those as well. 

Now, everytime the client sends something to the server, draw a line from the Client to the Server on the diagram, and put in the actual data that is being sent.  On the server side, look to see what variable the server is putting that data into, and write that off to the side of the server. 

Do the same thing for everything that the server sends to the client.  This should help you to keep track of what is sent and what is received.

Let's work through an example using this socket client and server.  This was a midterm question in previous terms, so is a good idea of what you might be expected to be able to do on a midterm. 

Beyond Sockets

You can see from this that it is possible to write client/server applications using raw sockets and using the BufferedReader and PrintWriter classes to perform unmarshalling and marshalling.  It is not, however, quite as easy as using something like RMI. 

Some of the disadvantages of raw sockets include:

Assignment 2-1

You should have the design finished for this assignment.  We'll take questions about the assignment tonight.  The code for the assignment will be due next week.

Next Week

Next week we will look at another way of communicating between client and server, RMI.