Section 10.5
Threads and Network Programming
NETWORK PROGRAMS ARE a natural application for threads. Threads were discussed in Section 7.6 in the context of GUI programming. (If you have not already read that section, it would be a good idea to do it now.) As we saw in that section, a thread could be used in a GUI program to perform a long computation in parallel with the event-handling thread of the GUI. Network programs with graphical user interfaces can use the same technique: If a separate thread is used for network communication, then the communication can proceed in parallel with other things that are going on in the program. Threads are even more important in server programs. In many cases, a client can remain connected to a server for an indefinite period of time. It's not a good idea to make other potential clients wait during this period. A multi-threaded server starts a new thread for each client. Several threads can run at the same time, so several clients can be served at the same time. The second client doesn't have to wait until the server is finished with the first client. It's like a post office that opens up a new window for each customer, instead of making them all wait in line at one window.
Now, there are at least two problems with the command-line chat examples, CLChatClient and CLChatServer, from the previous section. For one thing, after a user enters a message, the user must wait for a reply from the other side of the connection. It would be nice if the user could just keep typing lines and see the other user's messages as they arrive. It's not easy to do this in a command-line interface, but it's a natural application for a graphical user interface. The second problem has to do with opening connections in the first place. I can only run CLChatClient if I know that there is a CLChatServer running on some particular computer. Except in rather contrived situations, there is no way for me to know that. It would be nice if I could find out, somehow, who's out there waiting for a connection. In this section, we'll address both of these problems and, at the same time, learn a little more about network programming and about threads.
To address the first problem with the command-line chat programs, let's consider a GUI chat program. When one user connects to another user, a window should open on the screen with an input box where the user can enter messages to be transmitted to user on the other end of the connection. The user should be able to send a message at any time. The program should also be prepared to receive messages from the other side at any time, and those messages have to be displayed to the user as they arrive. In case this is not clear to you, here is an applet that simulates such a program. Enter a message in the input box at the bottom of the applet, and press return (or, equivalently, click the "Send" button):
Both incoming messages and messages that you send are posted to the JTextArea that occupies most of the applet. This is not a real network connection. When you send your first message, a separate thread is started by the applet. This thread simulates incoming messages from the other side of a network connection. In fact, it just chooses the messages at random from a pre-set list. At the same time, you can continue to enter and send messages. The run() method that is executed by the thread carries out the following algorithm:
Post the message "Hey, hello there! Nice to chat with you." while(running): Wait a random time, between 2 and 12 seconds Select a random message from the list Post the selected message in the JTextAreaThe variable running is set to false when the applet is stopped, as a signal to the thread that it should exit. The thread is created and started in the actionPerformed method that responds when you press return or click the "Send" button for the first time. You can find the complete source code in the file ChatSimulation.java, but I really want to look at the programming for the real thing rather than the simulation. The GUI chat program that we will look at is ChatWindow.java. The interface in this program will look similar to the simulation, but there will be a real network connection, and the incoming messages will be coming from the other side of that connection. The basic idea is not much more complicated than the simulation. A separate thread is created to wait for incoming messages and post them as they arrive. The run() method for this thread has an outline that is similar to the one for the simulation:
while the connection is open: Wait for a message to arrive from the other side Post the message in the JTextAreaHowever, the whole thing is complicated by the problem of opening and closing the connection and by the input/output errors that can occur at any time. The ChatWindow class is fairly sophisticated, and I don't want to cover everything that it does, but I will describe some of its functions. You should read the source code if you want to understand it completely.
First, there is the question of how a connection can be established between two ChatWindows. As the ChatWindow class is designed, the connection must be established before the window is opened. Recall that one end of a network connection is represented by on object of type Socket. The connected Socket is passed as a parameter to the ChatWindow constructor. This makes ChatWindow into a nicely reusable class that can be used in a variety of programs that set up the connection in different ways. The simplest approach to establishing the connection uses a command-line interface, just as is done with the CLChat programs. Once the connection has been established, a ChatWindow is opened on each side of the connection, and the actual chatting takes place through the windows instead of the command line. For this example, I've written a main() routine that can act as either the server or the client, depending on the command line argument that it is given. If the first command line argument is "-s", the program will act as a server. Otherwise, it assumes that the first argument specifies the computer where the server is running, and it acts as a client. The code for doing this is:
try { if (args[0].equalsIgnoreCase("-s")) { // Act as a server. Wait for a connection. ServerSocket listener = new ServerSocket(port); System.out.println("Listening on port " + listener.getLocalPort()); connection = listener.accept(); listener.close(); } else { // Act as a client. Request a connection with // a server running on the computer specified in args[0]. connection = new Socket(args[0],port); } out = new PrintWriter(connection.getOutputStream()); out.println(HANDSHAKE); out.flush(); in = new TextReader(connection.getInputStream()); message = in.getln(); if (! message.equals(HANDSHAKE) ) { throw new IOException( "Connected program is not a ChatWindow"); } System.out.println("Connected."); } catch (Exception e) { System.out.println("Error opening connection."); System.out.println(e.toString()); return; } ChatWindow w; // The window for this end of the connection. w = new ChatWindow("ChatWindow", connection);As it happens, I've taken the rather twisty approach of putting this main() routine in the ChatWindow class itself. (Possibly, it would be better style to put the main() routine in a different class.) This means that you can run ChatWindow as a standalone program. If you run it with the command "java ChatWindow -s", it will run as a server. To run it as a client, use the command "java ChatWindow <server>", where <server> is the name or IP number of the computer where the server is running. Use "localhost" as the name of the server, if you want to test the program by connecting to a server running on the same computer as the client. Whether the program is running as a client or as a server, once a connection is made, the window will open, and you can start chatting.
The constructor for the ChatWindow has the job of starting a thread to handle incoming messages. It also creates input and output streams for sending and receiving. The part of the constructor that performs these tasks look like this (with just a few changes for the sake of simplicity):
try { incoming = new TextReader( connection.getInputStream() ); outgoing = new PrintWriter( connection.getOutputStream() ); // Here, connection is the Socket that will be used for // communication. Input and output streams are created // for writing and reading information over the connection. } catch (IOException e) { // An error occurred while trying to get the streams. // Set up user interface to reflect the error. The // "transcript" is the JTextArea where messages are displayed. transcript.setText("Error opening I/O streams!\n" + "Connection can't be used.\n" + "You can close the window now.\n"); sendButton.setEnabled(false); connection = null; } /* Create the thread for reading data from the connection, unless an error just occurred. */ if (connection != null) { // Create a thread to execute the run() method in this // applet class, and start the thread. The run() method // will wait for incoming messages and post them to the // transcript when they are received. reader = new Thread(this); reader.start(); }The input stream, incoming, is used by the thread to read messages from the other side of the connection. It does this simply by saying incoming.getln(). This command will not return until a line of text has been received or until an error occurs. The output stream, outgoing, is used by the actionPerformed() method to transmit the text from the text input box.
When either user closes his ChatWindow, the connection must be closed on both sides. The connection might also be closed because an error occurs, such as a network failure. It takes some care to handle all this correctly. Take a look at the source code if you are interested.
There is still a big problem with running ChatWindow in the way I've just described. Suppose I want to set up a connection. How do I know who has a ChatWindow running as a server? If I start up the server myself, how will anyone know about it? The CLChat programs have the same problem. What I would like is a program that would show me a list of all the "chatters" who are available, and I would like to be able to add myself to the list so that other people can tell that I am available to receive connections. The problem is, who is going to keep the list and how will my program get a copy of the list?
This is a natural application for a server! We can have a server whose job is to keep a list of available chatters. This server can be run as a daemon on a "well-known computer", so that it is always available at a known location on the Internet. Then, a program anywhere on the Internet can contact the server to get the list of chatters or to register a person on the list. That program acts as a client for the server.
In fact, I've written such a server program. It's called ConnectionBroker, and the source code is available in the file ConnectionBroker.java. The main() routine of this server is similar to the main() routine of the DateServe example that was given at the beginning of this section. That is, it runs in an infinite loop in which it accepts connections and processes them. In this case, however, the processing of each request is much more complicated and can take a long time, so the main() routine sets up a separate thread to process each connection request. That's all the main routine does with the connection. The thread takes care of all the details, while the main program goes on to the next connection request. Here is the main() routine from ConnectionBroker:
public static void main(String[] args) { // The main() routine creates a listening socket and // listens for requests. When a request is received, // a thread is created to service it. int port; // Port on which server listens. ServerSocket listener; Socket client; if (args.length == 0) port = DEFAULT_PORT; else { try { port = Integer.parseInt(args[0]); } catch (NumberFormatException e) { System.out.println(args[0] + " is not a legal port number."); return; } } try { listener = new ServerSocket(port); } catch (IOException e) { System.out.println("Can't start server."); System.out.println(e.toString()); return; } System.out.println("Listening on port " + listener.getLocalPort()); try { while (true) { client = listener.accept(); // Get a connection request. new ClientThread(client); // Start a thread to handle it. } } catch (Exception e) { System.out.println("Server shut down unexpectedly."); System.out.println(e.toString()); System.exit(1); } }Once the processing thread has been started to handle the connection, the thread reads a command from the client, and carries out that command. It understands three types of commands:
- A REGISTER command that adds the client to the list of available chatters. The server keeps this list in an internal data structure. The connection remains open and the thread waits for some other user to request a connection with that client. Once a connection is made, the client is removed from the list.
- A SEND_CLIENT_LIST command requests a copy of the list of available chatters. The server responds by sending the list and closing the connection.
- A CONNECT command requests the server to set up a connection with one of the chatters in the list. The server sets up the connection, and -- if no error occurs -- informs both parties that a connection has been established. The two parties can then start sending messages to each other. (These messages actually continue to pass through the server. The direct network connections are between the server and the two clients. The server relays messages from each client to the other. It's done this way so that a ConnectionBroker will work with applets as clients, as long as the applets are loaded from the computer where the server is running. An applet is not ordinarily allowed to make network connections, except to the computer from which it was loaded.)
To use a ConnectionBroker, you need a program that acts as a client for the ConnectionBroker service. I have an applet that does this. The applet tries to connect to a ConnectionBroker server on the computer from which the applet was loaded. If no such server is running on that computer, the applet will display an error notification saying that it can't connect to the server. You are likely to get an error message unless you have downloaded this on-line textbook and are reading the copy on your own computer. In that case, you should be able to run the ConnectionBroker server on your computer and use the applet to connect to it. (Just compile ConnectionBroker.java and then give the command "java ConnectionBroker" in the same directory. It will print out "Listening on port 3030" and start waiting for connections. You will have to abort the program in some way to get it to end, such as by hitting CONTROL-C.) Here is the applet:
If the applet does find a server, it will display the list of available chatters in the JComboBox on the third line of the applet. If no chatters are available on the server, then you'll just see the message "(None available)". Once you register yourself, you will be included in this list, and you can open a connection to yourself. (Not a very interesting conversation perhaps, but it will demonstrate how the program works.) The procedures for registering yourself with the server and for requesting a connection to someone in the JComboBox should be easy enough to figure out. When you register yourself, a ChatWindow will open and will wait for someone to connect to you. A ChatWindow will also open when you request a connection.
You can enter yourself multiple times in the list, if you want, and you can connect to multiple people on the list. A separate ChatWindow will open for each connection.
This networked chat application is still very much a demonstration system. That is, it is not robust enough or full-featured enough to be used for serious applications. I tried to keep the interactions among the server, the applet, and the connection windows simple enough to understand with a reasonable effort. If you are interested in pursuing the topic of network programming, I suggest you start by reading the three source code files for this example: the applet BrokeredChat.java, the server ConnectionBroker.java, and the window ChatWindow.java.
The Problem of Synchronization
Although I don't want to say too much about the ConnectionBroker program, there is still one general question I want to look at: What happens when two or more threads use the same data? When this is the case, it's possible for the data to become corrupted, unless access to the data is carefully synchronized. The problem arises when two threads both try to access the data at the same time, or when one thread is interrupted by another when it is in the middle of accessing the data. Synchronization is used to make sure that this doesn't happen. To see what can go wrong, let's look at a typical example: a bank account. Suppose that the amount of money in a bank account is represented by the class:
public class BankAccount { private double balance; // amount of money in account public double getBalance() { return balance; } public void withdraw(double amount) { // Precondition: The balance is >= the amount. balance = balance - amount; } . . // Other methods . }Suppose that account is an object of type BankAccount, and that this variable is used by several threads. Suppose that one of these threads wants to do a withdrawal of $100. This should be easy:
if ( account.getBalance() >= 100) account.withdraw();But suppose that two threads try to do a withdrawal at the same time from an account that contains $150. It might happen that one thread calls account.getBalance() and gets a balance of 150. But at that moment, the first thread is interrupted by the other thread. The other thread calls account.getBalance() and also gets 150 as the balance. Both threads decide its safe to withdraw $100, but when they do so, the balance drops below zero. Actually, its even worse than this. The statement "balance = balance - amount" is actually executed as several steps: Read the balance; subtract the amount; store the new balance. It's possible for a thread to be interrupted in the middle of this. Suppose that two threads try to withdraw $100. If they execute the withdrawal at about the same time, it might happen that the order of operations is:
1. First thread reads the balance, and gets $150 2. Second thread reads the balance, and gets $150 3. Second thread subtracts $100 from $150, leaving $50 4. Second thread stores the new balance, $50 5. First thread (continuing after interruption) subtracts $100 from $150, leaving $50 6. First thread stores the new balance, $50The net result is that even though there have been two withdrawals of $100, the amount in the account has only gone down by one hundred. The bank will probably not be very happy with its programmers!
You might not think that this sequence of events is very likely, but when large numbers of computations are being performed by several threads on shared data, problems like this are almost certain to occur, and they can be disastrous when they happen. The synchronization problem is very real: Access to shared data must be controlled.
As I mentioned in Section 7.6, the Swing GUI library solves the synchronization problem in a straightforward way: Only one thread is allowed to change the data used by Swing components. That thread is the event-handling thread. If the some other thread wants to do something with a Swing component, it's not allowed to do it itself. It must arrange for the event-handling thread to do it instead. Swing has methods SwingUtilities.invokeLater() and SwingUtilities.invokeAndWait() to make this possible. This is the only type of synchronization that is used in the ChatSimulation, ChatWindow, and BrokeredChat programs.
In many cases, Swing's solution to the synchronization problem is not applicable and might even defeat the purpose of using multiple threads in the first place. Java has a more general means for controlling access to shared data. It's done using a new type of statement: the synchronized statement. A synchronized statement has the form:
synchronized ( <object-reference> ) { <statements> }For example:
synchronized(account) { if ( account.getBalance() >= amount ) balance = balance - amount; }The idea is that the <object-reference> -- account in the example -- is used to "lock" access to the statements. Each object in Java has a lock that can be used for synchronization. When a thread executes synchronized(account), it takes possession of account's lock, and will hold that lock until it is done executing the statements inside the synchronized statement. If a second thread tries to execute synchronized(account) while the first thread holds the lock, the second thread will have to wait until the first thread releases the lock. This means that it's impossible for two different threads to execute the statements in the synchronized statement at the same time. The scenarios that we looked at above, which could corrupt the data, are impossible.
It's possible to use the same object in two different synchronized statements. Only one of those statements can be executed at any given time, because all the statements require the same lock before they can be executed. By putting every access to some data inside synchronized statements, and using the same object for synchronization in each statement, we can make sure that that data will only be accessed by one thread at a time. This is the general approach for solving the synchronization problem. It is an approach that will work for multi-threaded servers, such as ConnectionBroker, where there are many threads that might need access to the same data. The ConnectionBroker program, for example, keeps a list of clients in a Vector named clientList. This vector is used by many threads, and access to it must be controlled. This is accomplished by putting all access to the vector in synchronized statements. The vector itself is used as the synchronization object (although there is no rule that says that the synchronization object has to be the same as the data that is being protected). Here, for your amusement is all the code from ConnectionBroker.java that accesses clientList:
/* These four methods synchronize access to a Vector, clientList, which contains a list of the clients of this server. The synchronization also protects the variable nextClientInfo. */ static void addClient(Client client) { // Adds a new client to the clientList vector. synchronized(clientList) { client.ID = nextClientID++; if (client.info.length() == 0) client.info = "Anonymous" + client.ID; clientList.addElement(client); } System.out.println("Added client " + client.ID + " " + client.info); } static void removeClient(Client client) { // Removes the client from the clientList, if present. synchronized(clientList) { clientList.removeElement(client); } System.out.println("Removed client " + client.ID); } static Client getClient(int ID) { // Removes client from the clientList vector, if it // contains a client of the given ID. If so, the // removed client is returned. Otherwise, null is returned. synchronized(clientList) { for (int i = 0; i < clientList.size(); i++) { Client c = (Client)clientList.elementAt(i); if (c.ID == ID) { clientList.removeElementAt(i); System.out.println("Removed client " + c.ID); c.ID = 0; // Since this client is no longer waiting! return c; } } return null; } } static Client[] getClients() { // Returns an array of all the clients in the // clientList. If there are none, null is returned. synchronized(clientList) { if (clientList.size() == 0) return null; Client[] clients = new Client[ clientList.size() ]; for (int i = 0; i < clientList.size(); i++) clients[i] = (Client)clientList.elementAt(i); return clients; } }You don't have to understand exactly what is going on here, just that the synchronized statements are used to control access to data that is being shared by multiple threads. There is much more to learn about threads; synchronization is only one of the problems that arise. However, I will leave the topic here. (One reason why I covered this much was to fulfill a promise made back in Section 3.6, where there was a list of all the different types of statements in Java. The synchronized statement was the last of these that we needed to cover.)
End of Chapter 10
[ Next Chapter | Previous Section | Chapter Index | Main Index ]