One of Java’s great strengths is painless networking. The
Java network library designers have made it quite similar to reading and
writing files, except that the “file” exists on a remote machine and the remote
machine can decide exactly what it wants to do about the information you’re
requesting or sending. As much as possible, the underlying details of networking
have been abstracted away and taken care of within the JVM and local machine
installation of Java. The programming model you use is that of a file; in fact,
you actually wrap the network connection (a “socket”) with stream objects, so
you end up using the same method calls as you do with all other streams. In
addition, Java’s built-in multithreading is exceptionally handy when dealing
with another networking issue: handling multiple connections at once.
This section introduces Java’s networking support using
easy-to-understand examples.
Of course, in order to tell one machine from another and to
make sure that you are connected with a particular machine, there must be some
way of uniquely identifying machines on a network. Early networks were
satisfied to provide unique names for machines within the local network.
However, Java works within the Internet, which requires a way to uniquely
identify a machine from all the others in the world. This is accomplished with
the IP (Internet Protocol) address which can exist in two forms“:
1. The
familiar DNS (Domain Name System) form. My domain name is bruceeckel.com, and
if I have a computer called Opus in my domain, its domain name would be
Opus.bruceeckel.com. This is exactly the kind of name that you use when you
send email to people, and is often incorporated into a World Wide Web address.
2. Alternatively,
you can use the dotted quad” form, which is four numbers separated by dots,
such as 123.255.28.120.
In both cases, the IP address is represented internally as a
32-bit number[1] (so each of the quad numbers cannot
exceed 255), and you can get a special Java object to represent this number
from either of the forms above by using the static
InetAddress.getByName( ) method that’s in java.net. The result is an
object of type InetAddress that you can use to build a “socket,” as you will
see later.
As a simple example of using InetAddress.getByName( ),
consider what happens if you have a dial-up Internet service provider (ISP).
Each time you dial up, you are assigned a temporary IP address. But while
you’re connected, your IP address has the same validity as any other IP address
on the Internet. If someone connects to your machine using your IP address then
they can connect to a Web server or FTP server that you have running on your
machine. Of course, they need to know your IP address, and since a new one is
assigned each time you dial up, how can you find out what it is?
The following program uses InetAddress.getByName( ) to
produce your IP address. To use it, you must know the name of your computer. On
Windows 95/98, go to “Settings,” “Control Panel,” “Network,” and then select
the “Identification” tab. “Computer name” is the name to put on the command
line.
//: c15:WhoAmI.java
// Finds out your network address when
// you're connected to the Internet.
// {RunByHand} Must be connected to the
Internet
// {Args: www.google.com}
import java.net.*;
public class WhoAmI {
public static void main(String[] args)
throws Exception {
if(args.length != 1) {
System.err.println(
"Usage: WhoAmI
MachineName");
System.exit(1);
}
InetAddress a =
InetAddress.getByName(args[0]);
System.out.println(a);
}
} ///:~
In this case, the machine is called “peppy.” So, once I’ve
connected to my ISP I run the program:
I get back a message like this (of course, the address is
different each time):
If I tell my friend this address and I have a Web server
running on my computer, he can connect to it by going to the URL
http://199.190.87.75 (only as long as I continue to stay connected during that
session). This can sometimes be a handy way to distribute information to
someone else, or to test out a Web site configuration before posting it to a
“real” server.
The whole point of a network is to allow two machines to
connect and talk to each other. Once the two machines have found each other
they can have a nice, two-way conversation. But how do they find each other? It’s
like getting lost in an amusement park: one machine has to stay in one place
and listen while the other machine says, “Hey, where are you?”
The machine that “stays in one place” is called the server,
and the one that seeks is called the client. This distinction is important only
while the client is trying to connect to the server. Once they’ve connected, it
becomes a two-way communication process and it doesn’t matter anymore that one
happened to take the role of server and the other happened to take the role of
the client.
So the job of the server is to listen for a connection, and
that’s performed by the special server object that you create. The job of the
client is to try to make a connection to a server, and this is performed by the
special client object you create. Once the connection is made, you’ll see that
at both server and client ends, the connection is magically turned into an I/O
stream object, and from then on you can treat the connection as if you were
reading from and writing to a file. Thus, after the connection is made you will
just use the familiar I/O commands from Chapter 11. This is one of the nice
features of Java networking.
For many reasons, you might not have a client machine, a
server machine, and a network available to test your programs. You might be
performing exercises in a classroom situation, or you could be writing programs
that aren’t yet stable enough to put onto the network. The creators of the
Internet Protocol were aware of this issue, and they created a special address
called localhost to be the “local loopback” IP address for testing without a
network. The generic way to produce this address in Java is:
InetAddress addr =
InetAddress.getByName(null);
If you hand getByName( ) a null, it defaults to using
the localhost. The InetAddress is what you use to refer to the particular
machine, and you must produce this before you can go any further. You can’t
manipulate the contents of an InetAddress (but you can print them out, as
you’ll see in the next example). The only way you can create an InetAddress is
through one of that class’s overloaded static member methods getByName( )
(which is what you’ll usually use), getAllByName( ), or
getLocalHost( ).
You can also produce the local loopback address by handing
it the string localhost:
InetAddress.getByName("localhost");
(assuming “localhost” is configured in your machine’s
“hosts” table), or by using its dotted quad form to name the reserved IP number
for the loopback:
InetAddress.getByName("127.0.0.1");
All three forms produce the same result.
An IP address isn’t enough to identify a unique server,
since many servers can exist on one machine. Each IP machine also contains
ports, and when you’re setting up a client or a server you must choose a port
where both client and server agree to connect; if you’re meeting someone, the
IP address is the neighborhood and the port is the bar.
The port is not a physical location in a machine, but a
software abstraction (mainly for bookkeeping purposes). The client program
knows how to connect to the machine via its IP address, but how does it connect
to a desired service (potentially one of many on that machine)? That’s where
the port numbers come in as a second level of addressing. The idea is that if
you ask for a particular port, you’re requesting the service that’s associated
with the port number. The time of day is a simple example of a service.
Typically, each service is associated with a unique port number on a given
server machine. It’s up to the client to know ahead of time which port number
the desired service is running on.
The system services reserve the use of ports 1 through 1024,
so you shouldn’t use those or any other port that you know to be in use. The
first choice for examples in this book will be port 8080 (in memory of the
venerable old 8-bit Intel 8080 chip in my first computer, a CP/M machine).
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 I/O 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 Socket through which communications will
take place on the server side. 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. These must be wrapped inside buffers and
formatting classes just like any other stream object described in Chapter 11.
The use of the term ServerSocket would seem to be another
example of a confusing naming 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. (However,
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.
These are converted to a Reader and a Writer, then wrapped in a BufferedReader
and a PrintWriter. After that, everything it reads from the BufferedReader it
echoes to the PrintWriter 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 and performs the same wrapping as in the server. Lines of text
are sent through the resulting PrintWriter. The client also creates an
InputStream (again, with appropriate conversions and wrapping) 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:
//: c15:JabberServer.java
// Very simple server that just
// echoes whatever the client sends.
// {RunByHand}
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 nonmemory 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 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 this 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
in and out 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:
//: c15:JabberClient.java
// Very simple client that just sends
// lines to the server and reads lines
// that the server sends.
// {RunByHand}
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:
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 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 unexplicitly 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 this 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:
//: c15:MultiJabberServer.java
// A server that uses multithreading
// to handle any number of clients.
// {RunByHand}
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) {
System.err.println("IO
Exception");
} finally {
try {
socket.close();
} catch(IOException e) {
System.err.println("Socket
not closed");
}
}
}
}
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 the creation 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. The maximum number of threads allowed is determined by the final
int MAX_THREADS.
//: c15:MultiJabberClient.java
// Client that tests the
MultiJabberServer
// by starting up multiple clients.
// {RunByHand}
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) {
System.err.println("Socket
failed");
// 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(