The programmer had to know many details
about the network and sometimes even the hardware. You usually needed to
understand the various “layers” of the networking protocol, and
there were a lot of different functions in each different networking library
concerned with connecting, packing, and unpacking blocks of information;
shipping those blocks back and forth; and handshaking. It was a daunting
task.
However, the concept of networking is not
so difficult. You want to get some information from that machine over there and
move it to this machine here, or vice versa. It’s 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.
One of Java’s great strengths is
painless networking. 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.
Of course, in order to tell one machine
from another and to make sure that you are connected with the machine you want,
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 that can exist in two forms:
In both
cases, the IP address is represented internally as a 32-bit
number[62] (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 it’s 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. It has been tested only on Windows 95, but
there you can go to “Settings,” “Control Panel,”
“Network,” and then select the “Identification” tab.
“Computer name” is the name to put on the command
line.
//: c14:WhoAmI.java // Finds out your network address when you're // connected to the Internet. 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 my case, the machine is called
“Colossus” (from the movie of the same name, because I keep putting
bigger disks on it). So, once I’ve connected to my ISP I run the
program:
java WhoAmI Colossus
I get back a message like this (of
course, the address is different each time):
Colossus/199.190.87.75
If I tell my friend this address, he can
log onto my personal Web server 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
just magically turned into an IO 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 IO commands from Chapter
10. 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 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");
or by using its dotted quad form to name
the reserved IP number for the loopback:
InetAddress.getByName("127.0.0.1");
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 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 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.)
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:
//: c14: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:
//: c14: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:
//: c14: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.
//: c14: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.
The examples you’ve seen so far use
the
Transmission
Control Protocol (TCP, also known as
stream-based
sockets), which is designed for ultimate reliability and guarantees that the
data will get there. It allows retransmission of lost data, it provides multiple
paths through different routers in case one goes down, and bytes are delivered
in the order they are sent. All this control and reliability comes at a cost:
TCP has a high overhead.
There’s a second protocol, called
User
Datagram Protocol (UDP), which doesn’t guarantee that the packets will
be delivered and doesn’t guarantee that they will arrive in the order they
were sent. It’s called an
“unreliable
protocol” (TCP is a
“reliable
protocol”), which sounds bad, but because it’s much faster it can be
useful. There are some applications, such as an audio signal, in which it
isn’t so critical if a few packets are dropped here or there but speed is
vital. Or consider a time-of-day server, where it really doesn’t matter if
one of the messages is lost. Also, some applications might be able to fire off a
UDP message to a server and can then assume, if there is no response in a
reasonable period of time, that the message was lost.
The support for datagrams in Java has the
same feel as its support for TCP sockets, but there are significant differences.
With datagrams, you put a
DatagramSocket on both
the client and server, but there is no analogy to the ServerSocket that
waits around for a connection. That’s because there is no
“connection,” but instead a datagram just shows up. Another
fundamental difference is that with TCP sockets, once you’ve made the
connection you don’t need to worry about who’s talking to whom
anymore; you just send the data back and forth through conventional streams.
However, with datagrams, the datagram packet must know where it came from and
where it’s supposed to go. That means you must know these things for each
datagram packet that you load up and ship off.
A DatagramSocket sends and
receives the packets, and the
DatagramPacket contains
the information. When you’re receiving a datagram, you need only provide a
buffer in which the data will be placed; the information about the Internet
address and port number where the information came from will be automatically
initialized when the packet arrives through the DatagramSocket. So the
constructor for a DatagramPacket to receive datagrams
is:
DatagramPacket(buf, buf.length)
in which buf is an array of
byte. Since buf is an array, you might wonder why the
constructor couldn’t figure out the length of the array on its own. I
wondered this, and can only guess that it’s a throwback to C-style
programming, in which of course arrays can’t tell you how big they
are.
You can reuse a receiving datagram; you
don’t have to make a new one each time. Every time you reuse it, the data
in the buffer is overwritten.
The maximum size of the buffer is
restricted only by the allowable datagram packet size, which limits it to
slightly less than 64Kbytes. However, in many applications you’ll want it
to be much smaller, certainly when you’re sending data. Your chosen packet
size depends on what you need for your particular application.
When you send a datagram, the
DatagramPacket must contain not only the data, but also the Internet
address and port where it will be sent. So the constructor for an outgoing
DatagramPacket is:
DatagramPacket(buf, length, inetAddress, port)
This time, buf (which is a byte
array) already contains the data that you want to send out. The
length might be the length of buf, but it can also be shorter,
indicating that you want to send only that many bytes. The other two arguments
are the Internet address where the packet is going and the destination port
within that machine.[63]
You might think that the two constructors
create two different objects: one for receiving datagrams and one for sending
them. Good OO design would suggest that these should be two different classes,
rather than one class with different behavior depending on how you construct the
object. This is probably true, but fortunately the use of DatagramPackets
is simple enough that you’re not tripped up by the problem, as you can see
in the following example. This example is similar to the
MultiJabberServer and MultiJabberClient example for TCP sockets.
Multiple clients will send datagrams to a server, which will echo them back to
the same client that sent the message.
To simplify the creation of a
DatagramPacket from a String and vice-versa, the example begins
with a utility class, Dgram, to do the work for you:
//: c14:Dgram.java // A utility class to convert back and forth // Between Strings and DataGramPackets. import java.net.*; public class Dgram { public static DatagramPacket toDatagram( String s, InetAddress destIA, int destPort) { // Deprecated in Java 1.1, but it works: byte[] buf = new byte[s.length() + 1]; s.getBytes(0, s.length(), buf, 0); // The correct Java 1.1 approach, but it's // Broken (it truncates the String): // byte[] buf = s.getBytes(); return new DatagramPacket(buf, buf.length, destIA, destPort); } public static String toString(DatagramPacket p){ // The Java 1.0 approach: // return new String(p.getData(), // 0, 0, p.getLength()); // The Java 1.1 approach: return new String(p.getData(), 0, p.getLength()); } } ///:~
The first method of Dgram takes a
String, an InetAddress, and a port number and builds a
DatagramPacket by copying the contents of the String into a
byte buffer and passing the buffer into the DatagramPacket
constructor. Notice the “+1” in the buffer allocation – this
was necessary to prevent truncation. The getBytes( ) method of
String is a special operation that copies the chars of a
String into a byte buffer. This method is now deprecated; Java
1.1 has a “better” way to do this but
it’s commented out here because it truncates the String. So
you’ll get a deprecation message when you compile it under Java 1.1, but
the behavior will be correct. (This bug might be fixed by the time you read
this.)
The Dgram.toString( ) method
shows both the Java 1.0 approach and the Java 1.1
approach (which is different because there’s a new kind of String
constructor).
Here is the server for the datagram
demonstration:
//: c14:ChatterServer.java // A server that echoes datagrams import java.net.*; import java.io.*; import java.util.*; public class ChatterServer { static final int INPORT = 1711; private byte[] buf = new byte[1000]; private DatagramPacket dp = new DatagramPacket(buf, buf.length); // Can listen & send on the same socket: private DatagramSocket socket; public ChatterServer() { try { socket = new DatagramSocket(INPORT); System.out.println("Server started"); while(true) { // Block until a datagram appears: socket.receive(dp); String rcvd = Dgram.toString(dp) + ", from address: " + dp.getAddress() + ", port: " + dp.getPort(); System.out.println(rcvd); String echoString = "Echoed: " + rcvd; // Extract the address and port from the // received datagram to find out where to // send it back: DatagramPacket echo = Dgram.toDatagram(echoString, dp.getAddress(), dp.getPort()); socket.send(echo); } } catch(SocketException e) { System.err.println("Can't open socket"); System.exit(1); } catch(IOException e) { System.err.println("Communication error"); e.printStackTrace(); } } public static void main(String[] args) { new ChatterServer(); } } ///:~
The ChatterServer contains a
single DatagramSocket for receiving messages, instead of creating one
each time you’re ready to receive a new message. The single
DatagramSocket can be used repeatedly. This DatagramSocket has a
port number because this is the server and the client must have an exact address
where it wants to send the datagram. It is given a port number but not an
Internet address because it resides on “this” machine so it knows
what its Internet address is (in this case, the default localhost). In
the infinite while loop, the socket is told to
receive( ),
whereupon it blocks until a datagram shows up, and then sticks it into our
designated receiver, the DatagramPacket dp. The packet is converted to a
String along with information about the Internet address and socket where
the packet came from. This information is displayed, and then an extra string is
added to indicate that it is being echoed back from the server.
Now there’s a bit of a quandary. As
you will see, there are potentially many different Internet addresses and port
numbers that the messages might come from – that is, the clients can
reside on any machine. (In this demonstration they all reside on the
localhost, but the port number for each client is different.) To send a
message back to the client that originated it, you need to know that
client’s Internet address and port number. Fortunately, this information
is conveniently packaged inside the
DatagramPacket that sent
the message, so all you have to do is pull it out using
getAddress( ) and
getPort( ), which
are used to build the DatagramPacket echo that is sent back
through the same socket that’s doing the receiving. In addition, when the
socket sends the datagram, it automatically adds the Internet address and port
information of this machine, so that when the client receives the
message, it can use getAddress( ) and getPort( ) to find
out where the datagram came from. In fact, the only time that
getAddress( ) and getPort( ) don’t tell you where
the datagram came from is if you create a datagram to send and you call
getAddress( ) and getPort( ) before you send the
datagram (in which case it tells the address and port of this machine, the one
the datagram is being sent from). This is an essential part of datagrams: you
don’t need to keep track of where a message came from because it’s
always stored inside the datagram. In fact, the most reliable way to program is
if you don’t try to keep track, but instead always extract the address and
port from the datagram in question (as is done here).
To test this server, here’s a
program that makes a number of clients, all of which fire datagram packets to
the server and wait for the server to echo them back.
//: c14:ChatterClient.java // Tests the ChatterServer by starting multiple // clients, each of which sends datagrams. import java.net.*; import java.io.*; public class ChatterClient extends Thread { // Can listen & send on the same socket: private DatagramSocket s; private InetAddress hostAddress; private byte[] buf = new byte[1000]; private DatagramPacket dp = new DatagramPacket(buf, buf.length); private int id; public ChatterClient(int identifier) { id = identifier; try { // Auto-assign port number: s = new DatagramSocket(); hostAddress = InetAddress.getByName("localhost"); } catch(UnknownHostException e) { System.err.println("Cannot find host"); System.exit(1); } catch(SocketException e) { System.err.println("Can't open socket"); e.printStackTrace(); System.exit(1); } System.out.println("ChatterClient starting"); } public void run() { try { for(int i = 0; i < 25; i++) { String outMessage = "Client #" + id + ", message #" + i; // Make and send a datagram: s.send(Dgram.toDatagram(outMessage, hostAddress, ChatterServer.INPORT)); // Block until it echoes back: s.receive(dp); // Print out the echoed contents: String rcvd = "Client #" + id + ", rcvd from " + dp.getAddress() + ", " + dp.getPort() + ": " + Dgram.toString(dp); System.out.println(rcvd); } } catch(IOException e) { e.printStackTrace(); System.exit(1); } } public static void main(String[] args) { for(int i = 0; i < 10; i++) new ChatterClient(i).start(); } } ///:~
ChatterClient is created as a
Thread so that multiple clients can be made to bother the server. Here
you can see that the receiving DatagramPacket looks just like the one
used for ChatterServer. In the constructor, the DatagramSocket is
created with no arguments since it doesn’t need to advertise itself as
being at a particular port number. The Internet address used for this socket
will be “this machine” (for the example, localhost) and the
port number will be automatically assigned, as you will see from the output.
This DatagramSocket, like the one for the server, will be used both for
sending and receiving.
The hostAddress is the Internet
address of the host machine you want to talk to. The one part of the program in
which you must know an exact Internet address and port number is the part in
which you make the outgoing DatagramPacket. As is always the case, the
host must be at a known address and port number so that clients can originate
conversations with the host.
Each thread is given a unique
identification number (although the port number automatically assigned to the
thread would also provide a unique identifier). In run( ), a message
String is created that contains the thread’s identification number
and the message number this thread is currently sending. This String is
used to create a datagram that is sent to the host at its address; the port
number is taken directly from a constant in ChatterServer. Once the
message is sent, receive( ) blocks until the server replies with an
echoing message. All of the information that’s shipped around with the
message allows you to see that what comes back to this particular thread is
derived from the message that originated from it. In this example, even though
UDP is an “unreliable” protocol, you’ll see that all of the
datagrams get where they’re supposed to. (This will be true for localhost
and LAN situations, but you might begin to see some failures for non-local
connections.)
When you run this program, you’ll
see that each of the threads finishes, which means that each of the datagram
packets sent to the server is turned around and echoed to the correct recipient;
otherwise one or more threads would hang, blocking until their input shows
up.
You might think that the only right way
to, for example, transfer a file from one machine to another is through TCP
sockets, since they’re “reliable.” However, because of the
speed of datagrams they can actually be a better solution. You simply break the
file up into packets and number each packet. The receiving machine takes the
packets and reassembles them; a “header packet” tells the machine
how many to expect and any other important information. If a packet is lost, the
receiving machine sends a datagram back telling the sender to
retransmit.
Traditional approaches to executing code
on other machines across a network have been confusing as well as tedious and
error-prone to implement. The nicest way to think about this problem is that
some object happens to live on another machine, and you can send a message to
that object and get a result as if the object lived on your local machine. This
simplification is exactly what Java 1.1
Remote Method Invocation
(RMI) allows you to do. This section walks you through the steps necessary to
create your own RMI objects.
RMI makes heavy use of interfaces. When
you want to create a remote object, you mask the underlying implementation by
passing around an interface. Thus, when the client gets a handle to a remote
object, what they really get is an interface handle, which happens to
connect to some local stub code that talks across the network. But you
don’t think about this, you just send messages via your interface
handle.
Here’s a simple
remote interface that represents an accurate time service:
//: c14:ptime:PerfectTimeI.java // The PerfectTime remote interface package c14.ptime; import java.rmi.*; interface PerfectTimeI extends Remote { long getPerfectTime() throws RemoteException; } ///:~
It looks like any other interface except
that it extends Remote and all of its methods throw
RemoteException. Remember that an interface and all of its methods
are automatically public.
The server must contain a class that
extends
UnicastRemoteObject and
implements the remote interface. This class can also have additional methods,
but only the methods in the remote interface will be available to the client, of
course, since the client will get only a handle to the interface, not the class
that implements it.
You must explicitly define the
constructor for the remote object even if you’re only defining a default
constructor that calls the base-class constructor. You must write it out since
it must throw RemoteException.
Here’s the implementation of the
remote interface PerfectTimeI:
//: c14:ptime:PerfectTime.java // The implementation of the PerfectTime // remote object package c14.ptime; import java.rmi.*; import java.rmi.server.*; import java.rmi.registry.*; import java.net.*; public class PerfectTime extends UnicastRemoteObject implements PerfectTimeI { // Implementation of the interface: public long getPerfectTime() throws RemoteException { return System.currentTimeMillis(); } // Must implement constructor to throw // RemoteException: public PerfectTime() throws RemoteException { // super(); // Called automatically } // Registration for RMI serving: public static void main(String[] args) { System.setSecurityManager( new RMISecurityManager()); try { PerfectTime pt = new PerfectTime(); Naming.bind( "//colossus:2005/PerfectTime", pt); System.out.println("Ready to do time"); } catch(Exception e) { e.printStackTrace(); } } } ///:~
Here, main( ) handles all the
details of setting up the server. When you’re serving RMI objects, at some
point in your program you must:
Here, you see a call to the static
method
Naming.bind( ).
However, this call requires that the registry be running as a separate process
on the computer. The name of the registry server is
rmiregistry, and under
32-bit Windows you say:
start rmiregistry
to start it in the background. On Unix,
it is:
rmiregistry &
Like many network programs, the
rmiregistry is located at the IP address of whatever machine started it
up, but it must also be listening at a port. If you invoke the
rmiregistry as above, with no argument, the registry’s port will
default to 1099. If you want it to be at some other port, you add an argument on
the command line to specify the port. For this example, the port will be located
at 2005, so the rmiregistry should be started like this under 32-bit
Windows:
start rmiregistry 2005
or for Unix:
rmiregistry 2005 &
The information about the port must also
be given to the bind( ) command, as well as the IP address of the
machine where the registry is located. But this brings up what can be a
frustrating problem if you’re expecting to test RMI programs locally the
way the network programs have been tested so far in this chapter. In the JDK
1.1.1 release, there are a couple of
problems:[64]
Will all this in
mind, the bind( ) command becomes:
Naming.bind("//colossus:2005/PerfectTime", pt);
If you are using the default port 1099,
you don’t need to specify a port, so you could say:
Naming.bind("//colossus/PerfectTime", pt);
In a future release of the JDK (after
1.1) when the localhost bug is fixed, you will be able to perform local
testing by leaving off the IP address and using only the
identifier:
Naming.bind("PerfectTime", pt);
The name for the service is arbitrary; it
happens to be PerfectTime here, just like the name of the class, but you could
call it anything you want. The important thing is that it’s a unique name
in the registry that the client knows to look for to procure the remote object.
If the name is already in the registry, you’ll get an
AlreadyBoundException. To
prevent this, you can always use
rebind( )
instead of bind( ), since rebind( ) either adds a new
entry or replaces the one that’s already there.
Even though main( ) exits,
your object has been created and registered so it’s kept alive by the
registry, waiting for a client to come along and request it. As long as the
rmiregistry is running and you don’t call
Naming.unbind( )
on your
name, the object will be there. For this reason, when you’re developing
your code you need to shut down the rmiregistry and restart it when you
compile a new version of your remote object.
You aren’t forced to start up
rmiregistry as an external process. If you know that your application is
the only one that’s going to use the registry, you can start it up inside
your program with the line:
LocateRegistry.createRegistry(2005);
Like before, 2005 is the port number we
happen to be using in this example. This is the equivalent of running
rmiregistry 2005 from a command line, but it can often be more convenient
when you’re developing RMI code since it eliminates the extra steps of
starting and stopping the registry. Once you’ve executed this code, you
can bind( ) using Naming as
before.
If you compile and run
PerfectTime.java, it won’t work even if you have the
rmiregistry running correctly. That’s because the framework for RMI
isn’t all there yet. You must first create the
stubs and
skeletons that provide the
network connection operations and allow you to pretend that the remote object is
just another local object on your machine.
What’s going on behind the scenes
is complex. Any objects that you pass into or return from a remote object must
implement Serializable
(if you want to pass remote references instead of the entire objects, the object
arguments can implement Remote), so you can imagine that the stubs and
skeletons are automatically performing serialization and deserialization as they
“marshal” all of the arguments across the network and return the
result. Fortunately, you don’t have to know any of this, but you do
have to create the stubs and skeletons. This is a simple process: you invoke the
rmic tool on your
compiled code, and it creates the necessary files. So the only requirement is
that another step be added to your compilation process.
The rmic tool is particular about
packages and classpaths,
however. PerfectTime.java is in the package c14.Ptime, and even if
you invoke rmic in the same directory in which PerfectTime.class
is located, rmic won’t find the file, since it searches the
classpath. So you must specify the location off the class path, like
so:
rmic c14.PTime.PerfectTime
You don’t have to be in the
directory containing PerfectTime.class when you execute this command, but
the results will be placed in the current directory.
When rmic runs successfully,
you’ll have two new classes in the directory:
PerfectTime_Stub.class PerfectTime_Skel.class
corresponding to the stub and skeleton.
Now you’re ready to get the server and client to talk to each
other.
The whole point of RMI is to make the use
of remote objects simple. The only extra thing that you must do in your client
program is to look up and fetch the remote interface from the server. From then
on, it’s just regular Java programming: sending messages to objects.
Here’s the program that uses PerfectTime:
//: c14:ptime:DisplayPerfectTime.java // Uses remote object PerfectTime package c14.ptime; import java.rmi.*; import java.rmi.registry.*; public class DisplayPerfectTime { public static void main(String[] args) { System.setSecurityManager( new RMISecurityManager()); try { PerfectTimeI t = (PerfectTimeI)Naming.lookup( "//colossus:2005/PerfectTime"); for(int i = 0; i < 10; i++) System.out.println("Perfect time = " + t.getPerfectTime()); } catch(Exception e) { e.printStackTrace(); } } } ///:~
The ID string is the same as the one used
to register the object with Naming, and the first part represents the URL
and port number. Since you’re using a URL, you can also specify a machine
on the Internet.
What comes back from
Naming.lookup( ) must be cast to the remote interface, not to
the class. If you use the class instead, you’ll get an
exception.
You can see in the method
call
t.getPerfectTime( )
that once you have a handle to the remote
object, programming with it is indistinguishable from programming with a local
object (with one difference: remote methods throw
RemoteException).
In large, distributed applications, your
needs might not be satisfied by the preceding approaches. For example, you might
want to interface with legacy datastores, or you might need services from a
server object regardless of its physical location. These situations require some
form of Remote Procedure Call (RPC), and possibly language independence. This is
where CORBA can help.
CORBA is not a
language feature; it’s an integration technology. It’s a
specification that vendors can follow to implement CORBA-compliant integration
products. CORBA is part of the OMG’s effort to define a standard framework
for distributed, language-independent object interoperability.
CORBA supplies the ability to make remote
procedure calls into Java objects and non-Java objects, and to interface with
legacy systems in a location-transparent way. Java adds networking support and a
nice object-oriented language for building graphical and non-graphical
applications. The Java and OMG object model map nicely
to each other; for example, both Java and CORBA implement the interface concept
and a reference object model.
The object interoperability specification
developed by the OMG is commonly referred to as the Object Management
Architecture (OMA). The OMA defines two components: the Core Object Model and
the OMA Reference Architecture. The Core Object Model states the basic concepts
of object, interface, operation, and so on. (CORBA is a refinement of the Core
Object Model.) The OMA Reference Architecture defines an underlying
infrastructure of services and mechanisms that allow objects to interoperate.
The OMA Reference Architecture includes the Object Request Broker (ORB), Object
Services (also known as CORBAservices), and common facilities.
The ORB is the communication bus by which
objects can request services from other objects, regardless of their physical
location. This means that what looks like a method call in the client code is
actually a complex operation. First, a connection with the server object must
exist, and to create a connection the ORB must know where the server
implementation code resides. Once the connection is established, the method
arguments must be marshaled, i.e. converted in a binary stream to be sent across
a network. Other information that must be sent are the server machine name, the
server process, and the identity of the server object inside that process.
Finally, this information is sent through a low-level wire protocol, the
information is decoded on the server side, and the call is executed. The ORB
hides all of this complexity from the programmer and makes the operation almost
as simple as calling a method on local object.
There is no specification for how an ORB
Core should be implemented, but to provide a basic compatibility among different
vendors’ ORBs, the OMG defines a set of services that are accessible
through standard interfaces.
CORBA is designed for language
transparency: a client object can call methods on a server object of different
class, regardless of the language they are implemented with. Of course, the
client object must know the names and signatures of methods that the server
object exposes. This is where IDL comes in. The CORBA IDL is a language-neutral
way to specify data types, attributes, operations, interfaces, and more. The IDL
syntax is similar to the C++ or Java syntax. The following table shows the
correspondence between some of the concepts common to three languages that can
be specified through CORBA IDL:
CORBA IDL |
Java |
C++ |
Module |
Package |
Namespace |
Interface |
Interface |
Pure abstract class |
Method |
Method |
Member function |
The inheritance concept is supported as
well, using the colon operator as in C++. The programmer writes an IDL
description of the attributes, methods, and interfaces that will be implemented
and used by the server and clients. The IDL is then compiled by a
vendor-provided IDL/Java compiler, which reads the IDL source and generates Java
code.
The IDL compiler is an extremely useful
tool: it doesn’t just generate a Java source equivalent of the IDL, it
also generates the code that will be used to marshal method arguments and to
make remote calls. This code, called the stub and skeleton code, is organized in
multiple Java source files and is usually part of the same Java package.
The naming service is one of the
fundamental CORBA services. A CORBA object is accessed through a reference, a
piece of information that’s not meaningful for the human reader. But
references can be assigned programmer-defined, string names. This operation is
known as stringifying the reference, and one of the OMA components, the
Naming Service, is devoted to performing string-to-object and object-to-string
conversion and mapping. Since the Naming Service acts as a telephone directory
that both servers and clients can consult and manipulate, it runs as a separate
process. Creating an object-to-string mapping is called binding an
object, and removing the mapping is called unbinding. Getting an
object reference passing a string is called resolving the
name.
For example, on startup, a server
application could create a server object, bind the object into the name service,
and then wait for clients to make requests. A client first obtains a server
object reference, resolving the string name, and then can make calls into the
server using the reference.
Again, the Naming Service specification
is part of CORBA, but the application that implements it is provided by the ORB
vendor. The way you get access to the Naming Service functionality can vary from
vendor to vendor.
The code shown here will not be elaborate
because different ORBs have different ways to access CORBA services, so examples
are vendor specific. (The example below uses JavaIDL, a free product from Sun
that comes with a light-weight ORB, a naming service, and an IDL-to-Java
compiler.) In addition, since Java is young and still evolving, not all CORBA
features are present in the various Java/CORBA products.
We want to implement a server, running on
some machine, that can be queried for the exact time. We also want to implement
a client that asks for the exact time. In this case we’ll be implementing
both programs in Java, but we could also use two different languages (which
often happens in real situations).
The first step is to write an IDL
description of the services provided. This is usually done by the server
programmer, who is then free to implement the server in any language in which a
CORBA IDL compiler exists. The IDL file is distributed to the client side
programmer and becomes the bridge between languages.
The example below shows the IDL
description of our ExactTime server:
//: c14:corba:ExactTime.idl //# You must install idltojava.exe from //# java.sun.com and adjust the settings to use //# your local C preprocessor in order to compile //# This file. See docs at java.sun.com. module remotetime { interface ExactTime { string getTime(); }; }; ///:~
This is a declaration of the
ExactTime interface inside the remotetime namespace. The interface
is made up of one single method that gives back the current time in
string format.
The second step is to compile the IDL to
create the Java stub and skeleton code that we’ll use for implementing the
client and the server. The tool that comes with the JavaIDL product is
idltojava:
idltojava remotetime.idl
This will automatically generate code for
both the stub and the skeleton. Idltojava generates a Java package
named after the IDL module, remotetime, and the generated Java files are
put in the remotetime subdirectory. _ExactTimeImplBase.java is the
skeleton that we’ll use to implement the server object, and
_ExactTimeStub.java will be used for the client. There are Java
representations of the IDL interface in ExactTime.java and a couple of
other support files used, for example, to facilitate access to the naming
service operations.
Below you can see the code for the server
side. The server object implementation is in the ExactTimeServer class.
The RemoteTimeServer is the application that creates a server object,
registers it with the ORB, gives a name to the object reference, and then sits
quietly waiting for client requests.
//: c14:corba:RemoteTimeServer.java import remotetime.*; import org.omg.CosNaming.*; import org.omg.CosNaming.NamingContextPackage.*; import org.omg.CORBA.*; import java.util.*; import java.text.*; // Server object implementation class ExactTimeServer extends _ExactTimeImplBase{ public String getTime(){ return DateFormat. getTimeInstance(DateFormat.FULL). format(new Date( System.currentTimeMillis())); } } // Remote application implementation public class RemoteTimeServer { public static void main(String args[]) { try { // ORB creation and initialization: ORB orb = ORB.init(args, null); // Create the server object and register it: ExactTimeServer timeServerObjRef = new ExactTimeServer(); orb.connect(timeServerObjRef); // Get the root naming context: org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Assign a string name to the // object reference (binding): NameComponent nc = new NameComponent("ExactTime", ""); NameComponent path[] = {nc}; ncRef.rebind(path, timeServerObjRef); // Wait for client requests: java.lang.Object sync = new java.lang.Object(); synchronized(sync){ sync.wait(); } } catch (Exception e) { System.out.println( "Remote Time server error: " + e); e.printStackTrace(System.out); } } } ///:~
As you can see, implementing the server
object is simple; it’s a regular Java class that inherits from the
skeleton code generated by the IDL compiler. Things get a bit more complicated
when it comes to interacting with the ORB and other CORBA
services.
This is a short description of what the
JavaIDL-related code is doing (primarily ignoring the part of the CORBA code
that is vendor dependent). The first line in main( ) starts up the
ORB, and of course, this is because our server object will need to interact with
it. Right after the ORB initialization, a server object is created. Actually,
the right term would be a transient servant object: an object that
receives requests from clients, and whose lifetime is the same as the process
that creates it. Once the transient servant object is created, it is registered
with the ORB, which means that the ORB knows of its existence and can now
forward requests to it.
Up to this point, all we have is
timeServerObjRef, an object reference that is known only inside the
current server process. The next step will be to assign a stringified name to
this servant object; clients will use that name to locate the servant object. We
accomplish this operation using the Naming Service. First, we need an object
reference to the Naming Service; the call to
resolve_initial_references( ) takes the stringified object reference
of the Naming Service that is “NameService,” in JavaIDL, and returns
an object reference. This is cast to a specific NamingContext reference
using the narrow( ) method. We can use now the naming
services.
To bind the servant object with a
stringified object reference, we first create a NameComponent object,
initialized with “ExactTime,” the name string we want to bind to the
servant object. Then, using the rebind( ) method, the stringified
reference is bound to the object reference. We use rebind( ) to
assign a reference, even if it already exists, whereas bind( )
raises an exception if the reference already exists. A name is made up in CORBA
by a sequence of NameContexts – that’s why we use an array to bind
the name to the object reference.
The servant object is finally ready for
use by clients. At this point, the server process enters a wait state. Again,
this is because it is a transient servant, so its lifetime is confined to the
server process. JavaIDL does not currently support persistent objects –
objects that survive the execution of the process that creates
them.
Now that we have an idea of what the
server code is doing, let’s look at the client code:
//: c14:corba:RemoteTimeClient.java import remotetime.*; import org.omg.CosNaming.*; import org.omg.CORBA.*; public class RemoteTimeClient { public static void main(String args[]) { try { // ORB creation and initialization: ORB orb = ORB.init(args, null); // Get the root naming context: org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Get (resolve) the stringified object // reference for the time server: NameComponent nc = new NameComponent("ExactTime", ""); NameComponent path[] = {nc}; ExactTime timeObjRef = ExactTimeHelper.narrow( ncRef.resolve(path)); // Make requests to the server object: String exactTime = timeObjRef.getTime(); System.out.println(exactTime); } catch (Exception e) { System.out.println( "Remote Time server error: " + e); e.printStackTrace(System.out); } } } ///:~
The first few lines do the same as they
do in the server process: the ORB is initialized and a reference to the naming
service server is resolved. Next, we need an object reference for the servant
object, so we pass the stringified object reference to the
resolve( ) method, and we cast the result into an ExactTime
interface reference using the narrow( ) method. Finally, we call
getTime( ).
Finally we have a server and a client
application ready to interoperate. You’ve seen that both need the naming
service to bind and resolve stringified object references. You must start the
naming service process before running either the server or the client. In
JavaIDL, the naming service is a Java application that comes with the product
package, but it can be different with other products. The JavaIDL naming service
runs inside an instance of the JVM and listens by default to network port
900.
Now you are ready to start your server
and client application (in this order, since our server is transient). If
everything is set up correctly, what you’ll get is a single output line on
the client console window, giving you the current time. Of course, this might be
not very exciting by itself, but you should take one thing into account: even if
they are on the same physical machine, the client and the server application are
running inside different virtual machines and they can communicate via an
underlying integration layer, the ORB and the Naming Service.
This is a simple example, designed to
work without a network, but an ORB is usually configured for location
transparency. When the server and the client are on different machines, the ORB
can resolve remote stringified references using a component known as the
Implementation Repository. Although the Implementation Repository is part
of CORBA, there is almost no specification, so it differs from vendor to
vendor.
As you can see, there is much more to
CORBA than what has been covered here, but you should get the basic idea. If you
want more information about CORBA, the place to start is the OMG Web site, at
http://www.omg.org. There you’ll find
documentation, white papers, proceedings, and references to other CORBA sources
and products.
Java applets can act as CORBA clients.
This way, an applet can access remote information and services exposed as CORBA
objects. But an applet can connect only with the server from which it was
downloaded, so all the CORBA objects the applet interacts with must be on that
server. This is the opposite of what CORBA tries to do: give you complete
location transparency.
This is an issue of network security. If
you’re on an Intranet, one solution is to loosen the security restrictions
on the browser. Or, set up a firewall policy for connecting with external
servers.
Some Java ORB products offer proprietary
solutions to this problem. For example, some implement what is called HTTP
Tunneling, while others have their special firewall features.
This is too complex a topic to be covered
in an appendix, but it is definitely something you should be aware
of.
You saw that one of the main CORBA
features is RPC support, which allows your local objects to call methods in
remote objects. Of course, there already is a native Java feature that does
exactly the same thing: RMI (see Chapter 15). While RMI makes RPC possible
between Java objects, CORBA makes RPC possible between objects implemented in
any language. It’s a big difference.
However, RMI can
be used to call services on remote, non-Java code. All you need is some kind of
wrapper Java object around the non-Java code on the server side. The wrapper
object connects externally to Java clients via RMI, and internally connects to
the non-Java code using one of the techniques shown above, such as JNI or
J/Direct.
This approach requires you to write a
kind of integration layer, which is exactly what CORBA does for you, but then
you don’t need a third-party
ORB.
This
section[65] gives
an overview of Sun Microsystems's Jini technology. It describes some Jini nuts
and bolts and shows how Jini's architecture helps to raise the level of
abstraction in distributed systems programming, effectively turning network
programming into object-oriented programming.
Traditionally, operating systems have
been designed with the assumption that a computer will have a processor, some
memory, and a disk. When you boot a computer, the first thing it does is look
for a disk. If it doesn't find a disk, it can't function as a computer.
Increasingly, however, computers are appearing in a different guise: as embedded
devices that have a processor, some memory, and a network connection – but
no disk. The first thing a cellphone does when you boot it up, for example, is
look for the telephone network. If it doesn't find the network, it can't
function as a cellphone. This trend in the hardware environment, from
disk-centric to network-centric, will affect how we organize our software
– and that's where Jini comes in.
Jini is an attempt to rethink computer
architecture, given the rising importance of the network and the proliferation
of processors in devices that have no disk drive. These devices, which will come
from many different vendors, will need to interact over a network. The network
itself will be very dynamic – devices and services will be added and
removed regularly. Jini provides mechanisms to enable smooth adding, removal,
and finding of devices and services on the network. In addition, Jini provides a
programming model that makes it easier for programmers to get their devices
talking to each other.
Building on top of Java, object
serialization, and RMI (which enable objects to move around the network from
virtual machine to virtual machine) Jini attempts to extend the benefits of
object-oriented programming to the network. Instead of requiring device vendors
to agree on the network protocols through which their devices can interact, Jini
enables the devices to talk to each other through interfaces to
objects.
Jini is a set of APIs and network
protocols that can help you build and deploy distributed systems that are
organized as federations of services. A service can be anything
that sits on the network and is ready to perform a useful function. Hardware
devices, software, communications channels – even human users themselves
– can be services. A Jini-enabled disk drive, for example, could offer a
“storage” service. A Jini-enabled printer could offer a
“printing” service. A federation of services, then, is a set of
services, currently available on the network, that a client (meaning a program,
service, or user) can bring together to help it accomplish some goal.
To perform a task, a client enlists the
help of services. For example, a client program might upload pictures from the
image storage service in a digital camera, download the pictures to a persistent
storage service offered by a disk drive, and send a page of thumbnail-sized
versions of the images to the printing service of a color printer. In this
example, the client program builds a distributed system consisting of itself,
the image storage service, the persistent storage service, and the
color-printing service. The client and services of this distributed system work
together to perform the task: to offload and store images from a digital camera
and print out a page of thumbnails.
The idea behind the word federation
is that the Jini view of the network doesn't involve a central controlling
authority. Because no one service is in charge, the set of all services
available on the network form a federation – a group composed of equal
peers. Instead of a central authority, Jini's runtime infrastructure merely
provides a way for clients and services to find each other (via a lookup
service, which stores a directory of currently available services). After
services locate each other, they are on their own. The client and its enlisted
services perform their task independently of the Jini runtime infrastructure. If
the Jini lookup service crashes, any distributed systems brought together via
the lookup service before it crashed can continue their work. Jini even includes
a network protocol that clients can use to find services in the absence of a
lookup service.
Jini defines a runtime infrastructure
that resides on the network and provides mechanisms that enable you to add,
remove, locate, and access services. The runtime infrastructure resides in three
places: in lookup services that sit on the network, in the service providers
(such as Jini-enabled devices), and in clients. Lookup services are the
central organizing mechanism for Jini-based systems. When new services become
available on the network, they register themselves with a lookup service. When
clients wish to locate a service to assist with some task, they consult a lookup
service.
The runtime infrastructure uses one
network-level protocol, called discovery, and two object-level protocols,
called join and lookup. Discovery enables clients and services to
locate lookup services. Join enables a service to register itself in a lookup
service. Lookup enables a client to query for services that can help accomplish
its goals.
Discovery works like this: Imagine you
have a Jini-enabled disk drive that offers a persistent storage service. As soon
as you connect the drive to the network, it broadcasts a presence
announcement by dropping a multicast packet onto a well-known port. Included
in the presence announcement is an IP address and port number where the disk
drive can be contacted by a lookup service.
Lookup services monitor the well-known
port for presence announcement packets. When a lookup service receives a
presence announcement, it opens and inspects the packet. The packet contains
information that enables the lookup service to determine whether or not it
should contact the sender of the packet. If so, it contacts the sender directly
by making a TCP connection to the IP address and port number extracted from the
packet. Using RMI, the lookup service sends an object, called a service
registrar, across the network to the originator of the packet. The purpose
of the service registrar object is to facilitate further communication with the
lookup service. By invoking methods on this object, the sender of the
announcement packet can perform join and lookup on the lookup service. In the
case of the disk drive, the lookup service would make a TCP connection to the
disk drive and would send it a service registrar object, through which the disk
drive would then register its persistent storage service via the join
process.
Once a service provider has a service
registrar object, the end product of discovery, it is ready to do a join –
to become part of the federation of services that are registered in the lookup
service. To do a join, the service provider invokes the register( )
method on the service registrar object, passing as a parameter an object called
a service item, a bundle of objects that describe the service. The register(
) method sends a copy of the service item up to the lookup service, where
the service item is stored. Once this has completed, the service provider has
finished the join process: its service has become registered in the lookup
service.
The service item is a container for
several objects, including an object called a service object, which
clients can use to interact with the service. The service item can also include
any number of attributes, which can be any object. Some potential
attributes are icons, classes that provide GUIs for the service, and objects
that give more information about the service.
Service objects usually implement one or
more interfaces through which clients interact with the service. For example, a
lookup service is a Jini service, and its service object is the service
registrar. The register( ) method invoked by service providers during
join is declared in the ServiceRegistrar interface (a member of the
net.jini.core.lookup package), which all service registrar objects
implement. Clients and service providers talk to the lookup service through the
service registrar object by invoking methods declared in the
ServiceRegistrar interface. Likewise, a disk drive would provide a
service object that implemented some well-known storage service interface.
Clients would look up and interact with the disk drive by this storage service
interface.
Once a service has registered with a
lookup service via the join process, that service is available for use by
clients who query that lookup service. To build a distributed system of services
that will work together to perform some task, a client must locate and enlist
the help of the individual services. To find a service, clients query lookup
services via a process called lookup.
To perform a lookup, a client invokes the
lookup( ) method on a service registrar object. (A client, like a service
provider, gets a service registrar through the previously-described process of
discovery.) The client passes as an argument to lookup( ) a service
template, an object that serves as search criteria for the query. The
service template can include a reference to an array of Class objects.
These Class objects indicate to the lookup service the Java type (or
types) of the service object desired by the client. The service template can
also include a service ID, which uniquely identifies a service, and
attributes, which must exactly match the attributes uploaded by the service
provider in the service item. The service template can also contain wildcards
for any of these fields. A wildcard in the service ID field, for example, will
match any service ID. The lookup( ) method sends the service template to
the lookup service, which performs the query and sends back zero to any matching
service objects. The client gets a reference to the matching service objects as
the return value of the lookup( ) method.
In the general case, a client looks up a
service by Java type, usually an interface. For example, if a client needed to
use a printer, it would compose a service template that included a Class
object for a well-known interface to printer services. All printer services
would implement this well-known interface. The lookup service would return a
service object (or objects) that implemented this interface. Attributes can be
included in the service template to narrow the number of matches for such a
type-based search. The client would use the printer service by invoking methods
from the well-known printer service interface on the service object.
Jini's architecture brings
object-oriented programming to the network by enabling network services to take
advantage of one of the fundamentals of objects: the separation of interface and
implementation. For example, a service object can grant clients access to the
service in many ways. The object can actually represent the entire service,
which is downloaded to the client during lookup and then executed locally.
Alternatively, the service object can serve merely as a proxy to a remote
server. Then when the client invokes methods on the service object, it sends the
requests across the network to the server, which does the real work. A third
option is for the local service object and a remote server to each do part of
the work.
One important consequence of Jini's
architecture is that the network protocol used to communicate between a proxy
service object and a remote server does not need to be known to the client. As
illustrated in the figure below, the network protocol is part of the service's
implementation. This protocol is a private matter decided upon by the developer
of the service. The client can communicate with the service via this private
protocol because the service injects some of its own code (the service object)
into the client's address space. The injected service object could communicate
with the service via RMI, CORBA, DCOM, some home-brewed protocol built on top of
sockets and streams, or anything else. The client simply doesn't need to care
about network protocols, because it can talk to the well-known interface that
the service object implements. The service object takes care of any necessary
communication on the network.
Different implementations of the same
service interface can use completely different approaches and network protocols.
A service can use specialized hardware to fulfill client requests, or it can do
all its work in software. In fact, the implementation approach taken by a single
service can evolve over time. The client can be sure it has a service object
that understands the current implementation of the service, because the client
receives the service object (by way of the lookup service) from the service
provider itself. To the client, a service looks like the well-known interface,
regardless of how the service is implemented.
Jini attempts to raise the level of
abstraction for distributed systems programming, from the network protocol level
to the object interface level. In the emerging proliferation of embedded devices
connected to networks, many pieces of a distributed system may come from
different vendors. Jini makes it unnecessary for vendors of devices to agree on
network level protocols that allow their devices to interact. Instead, vendors
must agree on Java interfaces through which their devices can interact. The
processes of discovery, join, and lookup, provided by the Jini runtime
infrastructure, will enable devices to locate each other on the network. Once
they locate each other, devices will be able to communicate with each other
through Java interfaces.
There’s actually a lot more to
networking than can be covered in this introductory treatment. Java networking
also provides fairly extensive support for URLs, including protocol handlers for
different types of content that can be discovered at an Internet site. You can
find other Java networking features fully and carefully described in Java
Network Programming by Elliotte Rusty Harold (O’Reilly,
1997).
[62]
This means a maximum of just over four billion numbers, which is rapidly running
out. The new standard for IP addresses will use a 128-bit number, which should
produce enough unique IP addresses for the foreseeable future.
[63]
TCP and UDP ports are considered unique. That is, you can simultaneously run a
TCP and UDP server on port 8080 without interference.
[64]
Many brain cells died in agony to discover this information.
[65]
This section was contributed by Bill Venners (www.artima.com)