Sockets
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.
These must be wrapped inside buffers and formatting classes just like any other
stream object described in Chapter 10.
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.)
A
simple server and client
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.)
//: 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.
//: 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:
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.