Scientific Programming II

Programming in Java

Networking


In this unit we will begin exploring how computers talk to each other over the network, using Java's stream-based communication over sockets. Sockets, just like files and STDIN/STDOUT, are represented in Java as streams, allowing us to easy use the data sent over the network.

We will be exploring the client/server relationship, where the client requests some action from the server, which performs the action and responds to the client. The most simple example of a client/server relationship is a simple chat application, allowing transmissions between the client AND the server.

Server.java

package edu.govschool.networking.simple;

import java.awt.BorderLayout;
import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

/**
 * Class to represent a server for our application.
 * @author Mr. Davis
 */
public class Server extends JFrame
{
    // Field to enter text
    private JTextField enterField;
    // Field to display the conversation
    private final JTextArea displayArea;
    // I/O Streams
    private ObjectOutputStream output;
    private ObjectInputStream input;
    // Socket to accept connections
    private ServerSocket server;
    // Socket to transmit to client
    private Socket connection;
    // Counter for the number of connections
    private int counter = 1;
    
    /**
     * Create a new Server. Only the GUI is initialized, runServer() must be
     * called to actually begin accepting connections
     */
    public Server()
    {
        // Create a new window with the title "Server"
        super("Server");
        
        // Setup the text field
        enterField = new JTextField();
        enterField.setEditable(false);
        // e.getActionCommand() gets the text from the field
        enterField.addActionListener((e) -> {
            sendData(e.getActionCommand());
            enterField.setText("");
        });
        add(enterField, BorderLayout.NORTH);
        
        // JScrollPane allows us to scroll our conversation
        displayArea = new JTextArea();
        add(new JScrollPane(displayArea));
        
        setSize(300, 150);
        setVisible(true);
    }
    
    /**
     * Begin accepting client connections.
     */
    public void runServer()
    {
        try {
            // Listen for connections on port 5000, while allowing
            // up to 100 clients to be queued up
            server = new ServerSocket(5000, 100);
            
            while (true) {
                try {
                    waitForConnection();
                    getStreams();
                    processConnection();
                } catch (EOFException e) {
                    displayMessage("\nServer terminated connection.");
                } finally {
                    closeConnection();
                    counter++;
                }
            }
        } catch (IOException e) {}
    }
    
    /**
     * Wait for a client connection. We display status messages to let
     * the user know what is happening.
     * @throws IOException the connection could not be made
     */
    private void waitForConnection() throws IOException
    {
        displayMessage("Waiting for connection\n");
        // The program will pause until a connection is made
        connection = server.accept();
        displayMessage("Connection " + counter + " received from: " +
                connection.getInetAddress().getHostName());
    }
    
    /**
     * Setup the streams from the client connection. The output
     * stream is flushed to prevent junk data from being sent over
     * the connection.
     * @throws IOException the streams could not be created
     */
    private void getStreams() throws IOException
    {
        // If we don't flush our output stream immediately, we may
        // send junk data
        output = new ObjectOutputStream(connection.getOutputStream());
        output.flush();
        
        input = new ObjectInputStream(connection.getInputStream());
        
        displayMessage("\nGot I/O streams\n");
    }
    
    /**
     * Update the display area when a message is received.
     * @throws IOException the connection failed for some reason
     */
    private void processConnection() throws IOException
    {
        // Send a success message over the network
        String msg = "Connection successful";
        sendData(msg);
        
        // Allow the writing of messages
        setTextFieldEditable(true);
        
        // Read messages in and display them, terminating if requested
        do {
            try {
                msg = (String) input.readObject();
                displayMessage("\n" + msg);
            } catch (ClassNotFoundException e) {
                displayMessage("\nUnknown object type received");
            }
        } while (!msg.equals("CLIENT>>> TERMINATE"));
    }
    
    /**
     * Close the I/O streams and the connection
     */
    private void closeConnection()
    {
        // Disallow messages to be send
        displayMessage("\nTerminating connection\n");
        setTextFieldEditable(false);
        
        // Close the streams and the socket
        try {
            output.close();
            input.close();
            connection.close();
        } catch (IOException e) {}
    }
    
    /**
     * Send a message over the connection. The output stream is flushed after
     * sending the message.
     */
    private void sendData(String msg)
    {
        try {
            // Write a message over the stream
            output.writeObject("SERVER>>> " + msg);
            // Flush the stream
            output.flush();
            // Append the message to our display area
            displayMessage("\nSERVER>>> " + msg);
        } catch (IOException e) {
            displayArea.append("\nError writing object");
        }
    }
    
    /**
     * Update the display area with a message. GUIs (whether Swing or
     * JavaFX) are NOT thread-safe, and if we want to support more than
     * one client in the future we need to interact with the GUI in a
     * thread-safe manner. Pre-Java 8 should use the commented version
     */
    private void displayMessage(final String msg)
    {
//        SwingUtilities.invokeLater(new Runnable() {
//            public void run() {
//                displayArea.append(msg);
//            }
//        });
        SwingUtilities.invokeLater(() -> displayArea.append(msg));
    }
    
    /**
     * Change the editable status of the text field.
     */
    private void setTextFieldEditable(final boolean editable)
    {
        SwingUtilities.invokeLater(() -> enterField.setEditable(editable));
    }
}

ServerTest.java

package edu.govschool.networking.simple;

import javax.swing.JFrame;

/**
 * Start a server.
 * @author Mr. Davis
 */
public class ServerTest 
{
    public static void main(String[] args)
    {
        Server app = new Server();
        app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        app.runServer();
    }
}

Client.java

package edu.govschool.networking.simple;

import java.awt.BorderLayout;
import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;



/**
 *
 * @author bryce
 */
public class Client extends JFrame
{
    private JTextField enterField;
    private JTextArea displayArea;
    private ObjectOutputStream output;
    private ObjectInputStream input;
    private String msg = "";
    private String chatServer;
    private Socket client;
    
    public Client(String host)
    {
        super("Client");
        
        enterField = new JTextField();
        enterField.setEditable(false);
        enterField.addActionListener((e) -> {
            sendData(e.getActionCommand());
            enterField.setText("");
        });
        add(enterField, BorderLayout.NORTH);
        
        displayArea = new JTextArea();
        add(new JScrollPane(displayArea));
        
        setSize(300, 150);
        setVisible(true);
        
        chatServer = host;
    }
    
    public void runClient()
    {
        try {
            connectToServer();
            getStreams();
            processConnection();
        } catch (EOFException eof) {
            displayMessage("\nClient terminated connection");
        } catch (IOException io) {}
        finally {
            closeConnection();
        }
    }
    
    private void connectToServer() throws IOException
    {
        displayMessage("Attempting connection\n");
        
        client = new Socket(InetAddress.getByName(chatServer), 5000);
        
        displayMessage("Connected to: " + 
                client.getInetAddress().getHostName());
    }
    
    private void getStreams() throws IOException
    {
        output = new ObjectOutputStream(client.getOutputStream());
        output.flush();
        
        input = new ObjectInputStream(client.getInputStream());
        
        displayMessage("\nGot I/O streams\n");
    }
    
    private void processConnection() throws IOException
    {
        setTextFieldEditable(true);
        
        do {
            try {
                msg = (String) input.readObject();
                displayMessage("\n" + msg);
            } catch (ClassNotFoundException e) {
                displayMessage("\nUnknown object type received");
            }
        } while (!msg.equals("SERVER>>> TERMINATE"));
    }
    
    private void closeConnection()
    {
        displayMessage("\nClosing connection");
        setTextFieldEditable(false);
        
        try {
            output.close();
            input.close();
            client.close();
        } catch (IOException e) {}
    }
    
    private void sendData(String mess)
    {
        try {
            output.writeObject("CLIENT>>> " + mess);
            output.flush();
            displayMessage("\nCLIENT>>> " + mess);
        } catch (IOException e) {
            displayArea.append("\nError writing object");
        }
    }
    
    private void displayMessage(final String mess)
    {
        SwingUtilities.invokeLater(() -> displayArea.append(mess));
    }
    
    private void setTextFieldEditable(final boolean edit)
    {
        SwingUtilities.invokeLater(() -> enterField.setEditable(edit));
    }
}

ClientTest.java

package edu.govschool.networking.simple;

import javax.swing.JFrame;

/**
 *
 * @author bryce
 */
public class ClientTest 
{
    public static void main(String[] args)
    {
        Client application;
        
        if (args.length == 0) {
            application = new Client("127.0.0.1");
        } else {
            application = new Client(args[0]);
        }
        
        application.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        application.runClient();
    }
}

Quite clearly we can see that this is not the best chat server application. In fact, we can barely even call it that, since we can only have one client! We can solve this problem, and support an almost unlimited number of clients, by using threads!

To do so, we need to re-develop both our Server and Client slightly, to use Threads. Instead of our main Server class handling the connected client, we will pass its connection off to a background Thread which will do all of the necessary processing. Additionally, our Client will use two threads, one for the GUI and the other for the connection.

Server.java

package edu.govschool.networking;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashSet;
import java.util.Set;

/**
 * Class to represent a chat server. The server runs as a command line
 * application, with no GUI.
 * @author Mr. Davis
 */
public class Server implements Runnable
{
    // The port to connect to
    private static final int PORT = 5000;
    // Thread to accept client connections in the background
    private Thread serverThread;
    // Set of output streams to clients
    private final Set<ObjectOutputStream> outputs;
    // ServerSocket for clients to connect to
    private ServerSocket server;
    // Temporary variable for client connection to pass to thread
    private Socket conn;
    // Variable for locking while broadcasting
    private boolean broadcasting = false;
    
    /**
     * Setup the server and begin accepting client connections.
     */
    public Server()
    {
        try {
            System.out.println("Binding to port " + PORT + ", please wait...");
            server = new ServerSocket(PORT);
            System.out.println("Server started: " + server);
        } catch (IOException e) {
            printErr("Error binding to port " + PORT + ".");
        }
        
        outputs = new HashSet<>();
    }
    
    public void runServer()
    {
        serverThread = new Thread(this);
        serverThread.start();
    }
    
    /**
     * Accept client connections as they appear. Once a client connects, its
     * connection (via a socket) is passed to a ClientThread for processing 
     * while connected.
     */
    @Override
    public void run()
    {
        while (true) {
            try {
                System.out.println("Waiting for a client...");
                // Accept a new client
                conn = server.accept();
                // Create a new ClientThread to handle the connection
                ClientThread user = new ClientThread(conn);
                // Notify that a client connected
                System.out.println("Client: " + user.getUsername() + 
                                                        " accepted " + conn);
                // Begin processing the client
                user.start();
            } catch (IOException e) {
                printErr("Error connecting client.");
            }
        }
    }
    
    /**
     * Print an error message.
     * @param err the error message
     */
    private void printErr(String err)
    {
        System.err.println(err);
    }
    
    /**
     * Send a message to the connected clients. We lock the outputs set, and 
     * release it afterwards. Clients will need to wait before disconnecting.
     * @param msg the message to broadcast
     * @throws IOException the message could not be broadcast
     */
    private synchronized void sendToClients(ChatResponse msg) throws IOException
    {
        for (ObjectOutputStream out : outputs) {
            out.writeObject(msg);
        }
    }
    
    private class ClientThread extends Thread
    {
        // The connection to the server
        private final Socket socket;
        // I/O streams
        private ObjectInputStream input;
        private ObjectOutputStream output;
        // The username of this client
        private String username;
        // The current message
        private ChatResponse line;
        
        public ClientThread(Socket socket)
        {
            // Save our connection
            this.socket = socket;
            
            // Setup the I/O streams
            try {
                this.getStreams();
                // Add the output stream to our set of outputs
                outputs.add(this.output);
            } catch (IOException e) {
                printErr("Error getting streams on client thread.");
            }
            
            // Set the client username
            try {
                this.line = (ChatResponse) input.readObject();
                this.username = this.line.getMessage();
            } catch (IOException | ClassNotFoundException e) {
                printErr("Error setting username on client thread.");
            }
        }
        
        /**
         * Get the client username.
         * @return the client username
         */
        public String getUsername()
        {
            return this.username;
        }
        
        /**
         * Setup our I/O streams via the socket connection.
         * @throws IOException the streams could not be created
         */
        private void getStreams() throws IOException
        {
            this.output = new ObjectOutputStream(socket.getOutputStream());
            this.output.flush();
            this.input = new ObjectInputStream(socket.getInputStream());
        }
        
        /**
         * Whenever a message is received from the input stream, broadcast it
         * to all available clients.
         */
        @Override
        public void run()
        {
            while (true) {
                try {
                    line = (ChatResponse) input.readObject();
                    
                    sendToClients(line);
                } catch (IOException | ClassNotFoundException e) {
                    printErr("Error sending message from client " + 
                                                                this.username);
                }
            }
        }
    }
    
    public static void main(String[] args)
    {
        new Server().runServer();
    }
}

Client.java

package edu.govschool.networking;

import java.awt.BorderLayout;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

/**
 * Class to represent a chat client. The client has a GUI window to chat with.
 * @author Mr. Davis
 */
public class Client extends JFrame implements Runnable
{
    // The port to connect to
    private static final int PORT = 5000;
    // The socket representing the connection to the server
    private Socket socket;
    // I/O representations
    private ObjectInputStream input;
    private ObjectOutputStream output;
    // Thread to handle the GUI
    private Thread thread;
    // GUI elements
    private JTextField entryField;
    private JTextArea displayArea;
    // Username
    private String username;
    // Message count
    private int count = 0;
    // Hostname/IP of the server
    private String serverHost;
    
    public Client()
    {
        // Create a simple JFrame with the title "Client"
        super("Client");
        
        // Setup the entry field
        entryField = new JTextField();
        entryField.addActionListener((e) -> {
            if (entryField.getText().equals("")) return;
            sendData(entryField.getText());
            entryField.setText("");
        });
        add(entryField, BorderLayout.SOUTH);
        
        // Setup the display area
        displayArea = new JTextArea(100, 40);
        displayArea.setEditable(false);
        add(new JScrollPane(displayArea), BorderLayout.NORTH);
        
        // Save our server host location
        this.serverHost = getServerHost();
        
        // Attempt to connect to the server
        try {
            socket = new Socket(serverHost, PORT);
        } catch (IOException e) {
            printErr("Error connection to server.");
        }
        
        // Attempt the setup the I/O streams
        try {
            getStreams();
        } catch (IOException e) {
            printErr("Error getting streams.");
        }
        
        // Attempt to send the username over the output stream
        try {
            // Get a username
            this.username = getUsername();
            ChatResponse name = new ChatResponse(ChatResponse.TYPE_USERNAME,
                                                 this.username);
            output.writeObject(name);
            output.flush();
        } catch (IOException e) {
            printErr("Error sending username to server.");
        }
        
        // Finalize the GUI and pass it to a background thread
        setSize(500, 500);
        setVisible(true);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    
    /**
     * Displays a dialog box prompting for the server's hostname/IP
     * @return 
     */
    private String getServerHost()
    {
        return JOptionPane.showInputDialog(this, 
                                           "Enter the server hostname/IP", 
                                           "Server host", 
                                           JOptionPane.QUESTION_MESSAGE);
    }
    
    /**
     * Displays a dialog box prompting for a username.
     * @return the chosen username
     */
    private String getUsername()
    {
        return JOptionPane.showInputDialog(this, 
                                            "Enter a user name", 
                                            "Getting username", 
                                            JOptionPane.QUESTION_MESSAGE);
    }
    
    /**
     * Send a message over the output stream.
     * @param msg the message to send
     */
    private void sendData(String msg)
    {
        try {
            ChatResponse message = new ChatResponse(ChatResponse.TYPE_MESSAGE,
                                                    this.username + ": " + msg);
            output.writeObject(message);
            output.flush();
        } catch (IOException e) {
            printErr("Error sending message to server.");
        }
    }
    
    /**
     * Print an error message.
     * @param err the error message
     */
    private void printErr(String err)
    {
        System.err.println(err);
    }
    
    /**
     * Setup our I/O streams via the socket connection. ENSURE YOU GET THE
     * OUTPUT STREAM FIRST.
     * @throws IOException the streams could not be created
     */
    private void getStreams() throws IOException
    {
        output = new ObjectOutputStream(socket.getOutputStream());
        output.flush();
        input = new ObjectInputStream(socket.getInputStream());
    }
    
    /**
     * Add a message to our display area.
     * @param msg the message to display
     */
    private void displayMessage(String msg)
    {
        SwingUtilities.invokeLater(() -> displayArea.append(msg + "\n"));
    }
    
    /**
     * Start the client thread.
     */
    public void runClient()
    {
        thread = new Thread(this);
        thread.start();
    }
    
    /**
     * Handle messages via the input stream as we receive them.
     */
    @Override
    public void run()
    {
        while (true) {
            try {
                // Read a message from the input stream.
                ChatResponse msg = (ChatResponse) input.readObject();

                displayMessage(msg.getMessage());
            } catch (IOException | ClassNotFoundException e) {
                printErr("Error reading message from server.");
            }
        }
    }
    
    public static void main(String[] args)
    {
        new Client().runClient();
    }
}

ChatResponse.java

package edu.govschool.networking;

import java.io.Serializable;

/**
 * Class representing a message over our chat network.
 * @author Mr. Davis
 */
public class ChatResponse implements Serializable
{
    public static final int TYPE_BAD_USERNAME  = 0x01;
    public static final int TYPE_GOOD_USERNAME = 0x02;
    public static final int TYPE_MESSAGE       = 0x03;
    public static final int TYPE_USERNAME      = 0x04;
    
    private final int type;
    private final String message;
    
    public ChatResponse(int type, String message)
    {
        this.type = type;
        this.message = message;
    }
    
    public int getResponseType()
    {
        return this.type;
    }
    
    public String getMessage()
    {
        return this.message;
    }
    
    public static ChatResponse badUsernameResponse()
    {
        return new ChatResponse(TYPE_BAD_USERNAME, "");
    }
    
    public static ChatResponse goodUsernameResponse()
    {
        return new ChatResponse(TYPE_GOOD_USERNAME, "");
    }
}