In the socket networking model, the server side has to read from or write to many sockets that are connected to many clients. We already know that by reading data from a socket in a separate thread, we solve the problem of hanging while we’re waiting for data. Threading on the server side has an additional benefit: by having a thread associated with each client, we no longer need to worry about other clients within any single thread. This simplifies our server-side programming: we can code our classes as if we were handling a single client at a time.
In this section, we’ll develop such a server. But before we dive right in, let us review some networking basics.
Figure 5.2 shows the data connections between several clients and a server. The server-side socket setup is implemented in two steps. First, a socket is used for the purpose of listening on a port known to the client. The client connects to this port as a means to negotiate a private connection to the server.
Once a data connection has been negotiated, the server and client then communicate through this private connection. In general, this process is generic: most programmers are concerned with the data sockets (the private connection). Furthermore, the data sockets on the server side are usually self-contained to a particular client. While it is possible to have different mechanisms that deal with many data sockets at the same time, generally the same code is used to deal with each of the data sockets independently of the other data sockets.
Since the setup is generic, we can place it into a generic TCPServer class and not have to implement the generic code again. Basically, this TCPServer class creates a ServerSocket and accepts connection requests from clients. This is done in a separate thread. Once a connection is made, the server clones (makes a copy of) itself so that it may handle the new client connection in a new thread:
import java.net.*; import java.io.*; public class TCPServer implements Cloneable, Runnable { Thread runner = null; ServerSocket server = null; Socket data = null; volatile boolean shouldStop = false; public synchronized void startServer(int port) throws IOException { if (runner == null) { server = new ServerSocket(port); runner = new Thread(this); runner.start(); } } public synchronized void stopServer() { if (server != null) { shouldStop = true; runner.interrupt(); runner = null; try { server.close(); } catch (IOException ioe) {} server = null; } } public void run() { if (server != null) { while (!shouldStop) { try { Socket datasocket = server.accept(); TCPServer newSocket = (TCPServer) clone(); newSocket.server = null; newSocket.data = datasocket; newSocket.runner = new Thread(newSocket); newSocket.runner.start(); } catch (Exception e) {} } } else { run(data); } } public void run(Socket data) { } }
Considering the number of threads started by the TCPServer class, the implementation of the class is simple. First, the TCPServer class implements the Runnable interface; we will be creating threads that this class will execute. Second, the class is cloneable, so that a copy of this class can be created for each connection. And since the copy of the class is also runnable, we can create another thread for each client connection. Since the original TCPServer object must operate on the server socket, and the clones must operate on the data sockets, the TCPServer class must be written to service both the server and data sockets.
To begin, once a TCPServer object has been instantiated, the
startServer()
method is called:
public synchronized void startServer(int port) throws IOException { if (runner == null) { server = new ServerSocket(port); runner = new Thread(this); runner.start(); } }
This method creates a ServerSocket object and a separate thread
to handle the ServerSocket object. By handling the ServerSocket in
another thread, the startServer()
method can
return immediately, and the same program can act as multiple servers.
We could have performed this initialization in the constructor of the
TCPServer class; there’s no particular reason why we chose to
do this in a separate method.
The
stopServer()
method is the cleanup
method for the TCPServer class:
public synchronized void stopServer() { if (server != null) { shouldStop = true; runner.interrupt(); runner = null; try { server.close(); } catch (IOException ioe) {} server = null; } }
This method cleans up what was done in the
startServer()
method. In this case, we need to
terminate the thread we started; we do that by setting the flag that
will be checked in the run()
method. In
addition, we interrupt that thread, in case the runner thread is
hanging in the accept()
method. Finally, we
close()
the socket that the thread was working
on.
We also set the runner
variable to
null
to allow the object to be reused: if the
runner
variable is null
,
the startServer()
method can be called later to
start another ServerSocket on the same port or on a different port.
Notice that the stopServer()
method also checks
to see if the server
variable is
null
before trying to stop the server. The
reason for this is that the TCPServer object will be cloned to handle
the data sockets. Since this clone handles a data socket, we set the
server
variable to null
in
the clone. This extra check is done just in case the programmer
decides to execute the stopServer()
method from
the clone instance that is handling a data socket.
The bulk of the logic comes in the run()
method:
public void run() { if (server != null) { while (!shouldStop) { try { Socket datasocket = server.accept(); TCPServer newSocket = (TCPServer) clone(); newSocket.server = null; newSocket.data = datasocket; newSocket.runner = new Thread(newSocket); newSocket.runner.start(); } catch (Exception e) {} } } else { run(data); } }
What is interesting about this class is that the
run()
method contains some conditional code.
Since the server instance variable is set in the
startServer()
method, the
if
statement in the run()
method always succeeds. Later, we will be cloning this TCPServer
object and starting more threads using the clone. The conditional
code differentiates the clone from the original.
The handling of the ServerSocket is straightforward. We just need to
accept()
connections from the clients. All the
details of binding to the socket and setting up the number of
listeners are handled by the ServerSocket class itself. Once we have
accepted a network connection from a client, we once again have a
situation that benefits from threading.
However, in this case, instead of using a different Runnable class,
we use the TCPServer class: more precisely, we clone our TCPServer
object and configure it to run as a runnable object in a newly
created thread. This is why the TCPServer’s
run()
method checks to see if a ServerSocket
object is available or not. The reason we cloned our TCPServer object
was so we can have private data for each thread. By making a copy of
the object, we make a copy of the instance variables that can then be
set to the values needed by the newly created thread.
All code that handles the ServerSocket is in the
while
loop of the run()
method. The rest of the run()
method handles the
client data socket:
public void run() {
if (server != null) {
...
} else {
run(data);
}
}
public void run(Socket data) {
}
The newly created thread running with the newly cloned runnable
object first calls the run()
method; for a data
socket, the run()
method just calls the
overloaded run(data)
method. As can be seen from
the code, this run(data)
method does absolutely
nothing; using the TCPServer class by itself does nothing with the
data sockets. To have a useful TCPServer, you must extend it:
import java.net.*; import java.io.*; public class ServerHandler extends TCPServer { public void run(Socket data) { try { InputStream is = data.getInputStream(); OutputStream os = data.getOutputStream(); // Process the data socket here. } catch (Exception e) {} } }
All we need to do in our subclass is override the
run(data)
method; we only need to handle one
data socket in the run(data)
method. We do not
have to worry about the ServerSocket or any of the other data
sockets. When the run(data)
method is called, it
is running in its own thread with its own copy of the TCP-Server
object. All the details of the ServerSocket and the other data
sockets are hidden from this instance of the TCPServer class.
Once we have developed a specific version of the TCPServer class (in this case, the ServerHandler class), we create an instance of the class and start the server. An example usage of the ServerHandler class is as follows:
import java.net.*; import java.io.*; public class MyServer { public static void main(String args[]) throws Exception { TCPServer serv = new ServerHandler(); serv.startServer(300); } }
Using this ServerHandler class is simple. We just need to instantiate
a TCPServer object and call its startServer()
method. Since the ServerHandler object is also a TCPServer object, it
behaves just like a TCPServer object; the only difference is that
each data socket will have code that is specific to the ServerHandler
class executed on its behalf.
What other threading issues, most notably synchronization
issues, are we concerned with in our TCPServer class?
Basically, there are no issues we
have not already seen. The startServer()
and
stopServer()
methods are synchronized because
they examine common instance variables that may change. The
run()
method does not have to be synchronized
because the startServer()
method is written to
guarantee that the run()
method is called only
once.
Since all the calls to the run()
method in each
connection are done in a clone()
of the
TCPServer object, there is no reason to synchronize the data socket
threads because they will be changing and examining different
instances of the TCP-Server class. The separate threads that handle
the data sockets are not sharing data and hence do not need to be
synchronized. And if the ServerHandler class needed to share data,
then the synchronization that would be done would be in the
ServerHandler or one of its supporting classes.
In this example, we used the Runnable interface technique. Could we have derived from the Thread class directly instead of using the Runnable interface? Yes, we could have. However, using the Runnable interface makes it possible for the TCPServer class to start another thread with a clone of itself. Deriving from the Thread class requires a different implementation. This implementation probably requires that a new TCPServer class be instantiated instead of simply cloned.
We are not keeping a reference of the “data
socket” thread objects anywhere; is this a problem?
It is not a problem. As noted earlier, the threading system keeps an
internal reference to every active thread in the system. As long as
the stop()
method has not been called on the
thread or the run()
method has not completed,
the thread is considered
active, and a
reference is kept somewhere in the threading system. While removing
all references to a thread object prevents the TCPServer from
arranging for this data socket thread to terminate, the garbage
collector cannot act on the thread object because the thread system
still has a reference to it.
Have you noticed that it is difficult to tell that the ServerHandler class and the MyServer class are threaded? This is the goal that we have been trying to achieve. Threads are a tool, and the threading system is a service. In the end, the classes we create are designed to accomplish a task. This class, if designed correctly, does not need to show what tools it is using. Our ServerHandler class just needs to specify code that will handle one data socket, and the MyServer class just needs to start the ServerHandler service. All the threading stuff is just implementation detail. This concept shouldn’t be that surprising: it’s one of the benefits of object-oriented programming.