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.
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));
}
}
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();
}
}
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));
}
}
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.
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();
}
}
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();
}
}
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, "");
}
}