The socket is the software abstraction used
to represent the “terminals” of a connection between two machines. For a given
connection, there’s a socket on each machine, and you can imagine a
hypothetical “cable” running between the two machines with each end of the
“cable” plugged into a socket. Of course, the physical hardware and cabling
between machines is completely unknown. The whole point of the abstraction is that
we don’t have to know more than is necessary.
In Java, you
create a socket to make the connection to the other machine, then you get an InputStream and OutputStream (or, with the appropriate converters, Reader and Writer) from the socket
in order to be able to treat the connection as an IO stream object. There are
two stream-based socket classes: a ServerSocket
that a server uses to “listen” for incoming connections and a Socket that a client uses in order to
initiate a connection. Once a client makes a socket connection, the ServerSocket returns (via the accept( ) method) a corresponding server side Socket through which direct
communications will take place. From then on, you have a true Socket to Socket connection and you treat both ends the same way because they
are the same. At this point, you use
the methods getInputStream( ) and getOutputStream( ) to produce the corresponding InputStream and OutputStream objects from each Socket.
The use of the
term ServerSocket would seem to be
another example of a confusing name scheme in the Java libraries. You might
think ServerSocket would be better
named “ServerConnector” or something without the word “Socket” in it. You might
also think that ServerSocket and Socket should both be inherited from
some common base class. Indeed, the two classes do have several methods in common
but not enough to give them a common base class. Instead, ServerSocket’s job is to wait until some other machine connects to
it, then to return an actual Socket.
This is why ServerSocket seems to be
a bit misnamed, since its job isn’t really to be a socket but instead to make a
Socket object when someone else
connects to it.
However, the ServerSocket does create a physical
“server” or listening socket on the host machine. This socket listens for
incoming connections and then returns an “established” socket (with the local
and remote endpoints defined) via the accept( )
method. The confusing part is that both of these sockets (listening and
established) are associated with the same server socket. The listening socket
can accept only new connection requests and not data packets. So while ServerSocket doesn’t make much sense
programmatically, it does “physically.”
When you
create a ServerSocket, you give it
only a port number. You don’t have to give it an IP address because it’s
already on the machine it represents. When you create a Socket, however, you must give both the IP address and the port
number where you’re trying to connect. (On the other hand, the Socket that comes back from ServerSocket.accept( ) already
contains all this information.)
This example
makes the simplest use of servers and clients using sockets. All the server
does is wait for a connection, then uses the Socket produced by that connection to create an InputStream and OutputStream. After that, everything it reads from the InputStream it echoes to the OutputStream until it receives the line
END, at which time it closes the connection.
The client
makes the connection to the server, then creates an OutputStream. Lines of text are sent through the OutputStream. The client also creates
an InputStream to hear what the
server is saying (which, in this case, is just the words echoed back).
Both the
server and client use the same port number and the client uses the local
loopback address to connect to the server on the same machine so you don’t have
to test it over a network. (For some configurations, you might need to be connected to a network for the programs
to work, even if you aren’t communicating over that network.)
Here is the
server:
//: JabberServer.java
// Very simple server that just
// echoes whatever the client sends.
import java.io.*;
import java.net.*;
public class JabberServer
{
// Choose a port outside of the range 1-1024:
public static final int PORT = 8080;
public static void main(String[] args)
throws IOException {
ServerSocket s = new ServerSocket(PORT);
System.out.println("Started: " + s);
try {
// Blocks until a connection occurs:
Socket socket = s.accept();
try {
System.out.println(
"Connection accepted: "+
socket);
BufferedReader in =
new BufferedReader(
new
InputStreamReader(
socket.getInputStream()));
// Output is automatically flushed
// by PrintWriter:
PrintWriter out =
new PrintWriter(
new
BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())),true);
while (true)
{
String str = in.readLine();
if (str.equals("END"))
break;
System.out.println("Echoing: " + str);
out.println(str);
}
// Always close the two sockets...
}
finally {
System.out.println("closing...");
socket.close();
}
} finally {
s.close();
}
}
}
You can see
that the ServerSocket just needs a
port number, not an IP address (since it’s running on this machine!). When you call accept( ),
the method blocks until some client
tries to connect to it. That is, it’s there waiting for a connection but other
processes can run (see Chapter 14). When a connection is made, accept( ) returns with a Socket object representing that
connection.
The
responsibility for cleaning up the sockets is crafted carefully here. If the ServerSocket constructor fails, the
program just quits (notice we must assume that the constructor for ServerSocket doesn’t leave any open
network sockets lying around if it fails). For this case, main( ) throws IOException so a try block is not necessary. If the ServerSocket constructor is successful then all other method calls
must be guarded in a try-finally
block to ensure that, no matter how the block is left, the ServerSocket is properly closed.
The same logic
is used for the Socket returned by accept( ). If accept( ) fails, then we must assume that the Socket doesn’t exist or hold any
resources, so it doesn’t need to be cleaned up. If it’s successful, however,
the following statements must be in a try-finally
block so that if they fail the Socket
will still be cleaned up. Care is required here because sockets use important
non-memory resources, so you must be diligent in order to clean them up (since
there is no destructor in Java to do it for you).
Both the ServerSocket and the Socket produced by accept( ) are printed to System.out.
This means that their toString( )
methods are automatically called. These produce:
ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]
Socket[addr=127.0.0.1,PORT=1077,localport=8080]
Shortly, you’ll see how these fit
together with what the client is doing.
The next part
of the program looks just like opening files for reading and writing except
that the InputStream and OutputStream are created from the Socket object. Both the InputStream and OutputStream objects are converted to Java 1.1 Reader and Writer objects using the “converter” classes InputStreamReader and OutputStreamWriter, respectively. You could also have used the Java
1.0 InputStream and OutputStream classes directly, but with output there’s a distinct
advantage to using the Writer
approach. This appears with PrintWriter, which has an overloaded constructor that takes a
second argument, a boolean flag that
indicates whether to automatically flush the output at the end of each println( ) (but not print( ))
statement. Every time you write to out,
its buffer must be flushed so the information goes out over the network.
Flushing is important for this particular example because the client and server
each wait for a line from the other party before proceeding. If flushing
doesn’t occur, the information will not be put onto the network until the
buffer is full, which causes lots of problems in this example.
When writing
network programs you need to be careful about using automatic flushing. Every
time you flush the buffer a packet must be created and sent. In this case,
that’s exactly what we want, since if the packet containing the line isn’t sent
then the handshaking back and forth between server and client will stop. Put
another way, the end of a line is the end of a message. But in many cases
messages aren’t delimited by lines so it’s much more efficient to not use auto
flushing and instead let the built-in buffering decide when to build and send a
packet. This way, larger packets can be sent and the process will be faster.
Note that,
like virtually all streams you open, these are buffered. There’s an exercise at
the end of the chapter to show you what happens if you don’t buffer the streams
(things get slow).
The infinite while loop reads lines from the BufferedReader in and writes
information to System.out and to the
PrintWriter out. Note that these could be any streams, they just happen to be
connected to the network.
When the
client sends the line consisting of “END” the program breaks out of the loop
and closes the Socket.
Here’s the
client:
//: JabberClient.java
// Very simple client that just sends
// lines to the server and reads lines
// that the server sends.
import java.net.*;
import java.io.*;
public class JabberClient
{
public static void main(String[] args)
throws IOException {
// Passing null to getByName() produces the
// special "Local Loopback" IP address, for
// testing on one machine w/o a network:
InetAddress addr =
InetAddress.getByName(null);
// Alternatively, you can use
// the address or name:
// InetAddress addr =
InetAddress.getByName("127.0.0.1");
// InetAddress addr = InetAddress.getByName("localhost");
System.out.println("addr = " + addr);
Socket socket =
new Socket(addr, JabberServer.PORT);
// Guard everything in a try-finally to make
// sure that the socket is closed:
try {
System.out.println("socket = " + socket);
BufferedReader in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Output is automatically flushed
// by
PrintWriter:
PrintWriter out =
new PrintWriter(
new BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())),true);
for(int i = 0; i < 10; i ++) {
out.println("howdy
" + i);
String str = in.readLine();
System.out.println(str);
}
out.println("END");
} finally {
System.out.println("closing...");
socket.close();
}
}
}
In main( ) you can see all three ways
to produce the InetAddress of the
local loopback IP address: using null,
localhost, or the explicit reserved
address 127.0.0.1. Of course, if you
want to connect to a machine across a network you substitute that machine’s IP
address. When the InetAddress addr
is printed (via the automatic call to its toString( )
method) the result is:
localhost/127.0.0.1
By handing getByName( ) a null, it defaulted to finding the localhost, and that produced the
special address 127.0.0.1.
Note that the Socket called socket is created with both the InetAddress and the port number. To understand what it means when
you print out one of these Socket objects,
remember that an Internet connection is determined uniquely by these four
pieces of data: clientHost, clientPortNumber, serverHost, and serverPortNumber.
When the server comes up, it takes up its assigned port (8080) on the localhost
(127.0.0.1). When the client comes up, it is allocated to the next available
port on its machine, 1077 in this case, which also happens to be on the same
machine (127.0.0.1) as the server. Now, in order for data to move between the
client and server, each side has to know where to send it. Therefore, during
the process of connecting to the “known” server, the client sends a “return
address” so the server knows where to send its data. This is what you see in
the example output for the server side:
Socket[addr=127.0.0.1,port=1077,localport=8080]
This means that the server just accepted a connection from 127.0.0.1 on port 1077 while listening on its local port (8080). On the client side:
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]
which means that the client made
a connection to 127.0.0.1 on port 8080 using the local port 1077.
You’ll notice that every time you start up the client anew, the local port number is incremented. It starts at 1025 (one past the reserved block of ports) and keeps going up until you reboot the machine, at which point it starts at 1025 again. (On UNIX machines, once the upper limit of the socket range is reached, the numbers will wrap around to the lowest available number again.)
Once the Socket object has been created, the
process of turning it into a BufferedReader
and PrintWriter is the same as in
the server (again, in both cases you start with a Socket). Here, the client initiates the conversation by sending the
string “howdy” followed by a number. Note that the buffer must again be flushed
(which happens automatically via the second argument to the PrintWriter constructor). If the buffer
isn’t flushed, the whole conversation will hang because the initial “howdy”
will never get sent (the buffer isn’t full enough to cause the send to happen
automatically). Each line that is sent back from the server is written to System.out to verify that everything is
working correctly. To terminate the conversation, the agreed-upon “END” is
sent. If the client simply hangs up, then the server throws an exception.
You can see
that the same care is taken here to ensure that the network resources
represented by the Socket are
properly cleaned up, using a try-finally
block.
Sockets
produce a “dedicated” connection that
persists until it is explicitly disconnected. (The dedicated connection can
still be disconnected un-explicitly if one side, or an intermediary link, of
the connection crashes.) This means the two parties are locked in communication
and the connection is constantly open. This seems like a logical approach to
networking, but it puts an extra load on the network. Later in the chapter
you’ll see a different approach to networking, in which the connections are
only temporary.
The JabberServer works, but it can handle only one client at a time. In
a typical server, you’ll want to be able to deal with many clients at once. The
answer is multithreading, and in languages that don’t
directly support multithreading this means all sorts of complications. In
Chapter 14 you saw that multithreading in Java is about as simple as possible,
considering that multithreading is a rather complex topic. Because threading in
Java is reasonably straightforward, making a server that handles multiple
clients is relatively easy.
The basic
scheme is to make a single ServerSocket
in the server and call accept( )
to wait for a new connection. When accept( )
returns, you take the resulting Socket
and use it to create a new thread whose job is to serve that particular client.
Then you call accept( ) again
to wait for a new client.
In the
following server code, you can see that it looks similar to the JabberServer.java example except that
all of the operations to serve a particular client have been moved inside a
separate thread class:
//: MultiJabberServer.java
// A server that uses multithreading to
handle
// any number of clients.
import java.io.*;
import java.net.*;
class ServeOneJabber extends Thread {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
public ServeOneJabber(Socket s)
throws IOException {
socket = s;
in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Enable auto-flush:
out =
new PrintWriter(
new BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())), true);
// If any of the above calls throw an
// exception, the caller is responsible for
// closing the socket. Otherwise the thread
// will close it.
start(); // Calls run()
}
public void run() {
try {
while (true) {
String str = in.readLine();
if (str.equals("END"))
break;
System.out.println("Echoing: " + str);
out.println(str);
}
System.out.println("closing...");
} catch (IOException e) {
} finally {
try {
socket.close();
}
catch(IOException e) {}
}
}
}
public class
MultiJabberServer {
static final int PORT
= 8080;
public static void main(String[] args)
throws IOException {
ServerSocket s = new ServerSocket(PORT);
System.out.println("Server Started");
try {
while(true) {
// Blocks until a connection occurs:
Socket socket = s.accept();
try {
new ServeOneJabber(socket);
} catch(IOException e) {
//
If it fails, close the socket,
// otherwise the thread will close it:
socket.close();
}
}
} finally {
s.close();
}
}
}
The ServeOneJabber thread takes the Socket object that’s produced by accept( ) in main( ) every time a new client makes a connection. Then, as
before, it creates a BufferedReader
and auto-flushed PrintWriter object
using the Socket. Finally, it calls
the special Thread method start( ), which performs thread
initialization and then calls run( ).
This performs the same kind of action as in the previous example: reading
something from the socket and then echoing it back until it reads the special
“END” signal.
The
responsibility for cleaning up the socket must again be carefully designed. In
this case, the socket is created outside of the ServeOneJabber so the responsibility can be shared. If the ServeOneJabber constructor fails, it
will just throw the exception to the caller, who will then clean up the thread.
But if the constructor succeeds, then the ServeOneJabber
object takes over responsibility for cleaning up the thread, in its run( ).
Notice the
simplicity of the MultiJabberServer.
As before, a ServerSocket is created
and accept( ) is called to
allow a new connection. But this time, the return value of accept( ) (a Socket)
is passed to the constructor for ServeOneJabber,
which creates a new thread to handle that connection. When the connection is
terminated, the thread simply goes away.
If the
creation of the ServerSocket fails,
the exception is again thrown through main( ).
But if it succeeds, the outer try-finally
guarantees its cleanup. The inner try-catch
guards only against the failure of the ServeOneJabber
constructor; if the constructor succeeds, then the ServeOneJabber thread will close the associated socket.
To test that
the server really does handle multiple clients, the following program creates
many clients (using threads) that connect to the same server. Each thread has a
limited lifetime, and when it goes away, that leaves space for the creation of
a new thread. The maximum number of threads allowed is determined by the final int maxthreads. You’ll notice
that this value is rather critical, since if you make it too high the threads
seem to run out of resources and the program mysteriously fails.
//: MultiJabberClient.java
// Client that tests the
MultiJabberServer
// by starting up multiple clients.
import java.net.*;
import java.io.*;
class JabberClientThread extends Thread {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private static int counter = 0;
private int id =
counter++;
private static int threadcount = 0;
public static int threadCount() {
return threadcount;
}
public JabberClientThread(InetAddress addr) {
System.out.println("Making client " + id);
threadcount++;
try {
socket = new Socket(addr,
MultiJabberServer.PORT);
} catch(IOException e) {
// If the
creation of the socket fails,
// nothing needs to be cleaned up.
}
try {
in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Enable auto-flush:
out =
new PrintWriter(
new BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())), true);
start();
} catch(IOException e) {
// The socket should be closed on any
// failures other than the socket
// constructor:
try {
socket.close();
}
catch(IOException e2) {}
}
// Otherwise the socket will be closed by
// the run() method of the thread.
}
public void run() {
try {
for(int i = 0; i < 25; i++) {
out.println("Client
" + id + ": " + i);
String str = in.readLine();
System.out.println(str);
}
out.println("END");
} catch(IOException e) {
} finally {
// Always close it:
try {
socket.close();
}
catch(IOException e) {}
threadcount--; // Ending this thread
}
}
}
public class
MultiJabberClient {
static final int
MAX_THREADS = 40;
public static void main(String[] args)
throws IOException, InterruptedException {
InetAddress addr =
InetAddress.getByName(null);
while(true) {
if(JabberClientThread.threadCount()
< MAX_THREADS)
new JabberClientThread(addr);
Thread.currentThread().sleep(100);
}
}
}
The JabberClientThread constructor takes an
InetAddress and uses it to open a Socket. You’re probably starting to see
the pattern: the Socket is always
used to create some kind of Reader and/or
Writer (or InputStream and/or OutputStream)
object, which is the only way that the Socket
can be used. (You can, of course, write a class or two to automate this process
instead of doing all the typing if it becomes painful.) Again, start( ) performs thread initialization
and calls run( ). Here,
messages are sent to the server and information from the server is echoed to
the screen. However, the thread has a limited lifetime and eventually
completes. Note that the socket is cleaned up if the constructor fails after
the socket is created but before the constructor completes. Otherwise the
responsibility for calling close( )
for the socket is relegated to the run( )
method.
The threadcount keeps track of how many JabberClientThread objects currently
exist. It is incremented as part of the constructor and decremented as run( ) exits (which means the
thread is terminating). In MultiJabberClient.main( ),
you can see that the number of threads is tested, and if there are too many, no
more are created. Then the method sleeps. This way, some threads will
eventually terminate and more can be created. You can experiment with MAX_THREADS to see where your
particular system begins to have trouble with too many connections.