by Michael Morrison
The networking capabilities of Java are perhaps the most powerful component of the Java API because the vast majority of Java programs run in a networked environment. Using the wide range of network features built into Java, you can easily develop Web-based applets that perform a variety of tasks over a network. The network support in Java is particularly well suited to a client/server arrangement where a server marshals information and serves it to clients that handle the details of displaying the information to a user.
In today's lesson you'll learn what Java has to offer in regard to communicating over an Internet network connection using a client/server arrangement. You'll begin the lesson by taking a look at some basic concepts surrounding the structure of the Internet as a network. You'll then move on to what specific support is provided by the standard Java networking API and how it fits in with the client/server paradigm. Finally, you'll conclude the lesson by developing a couple of interesting sample programs demonstrating the different types of client/server approaches available in Java.
The following topics are covered in today's lesson:
By the end of this lesson, you'll be ready to build your own Java network client/server programs from scratch. You'll also have a better understanding of one of the reasons Java has become so popular-by virtue of its clean and straightforward support for an otherwise messy and complex area of programming: network programming!
Before you learn about the types of network support Java provides, it's important that you understand some fundamentals about the structure of the Internet as a network. As you are no doubt already aware, the Internet is a global network of many different types of computers connected in various ways. With this wide diversity of both hardware and software all connected together, it's pretty amazing that the Internet is even functional. Trust me, the functionality of the Internet is no accident and has come at no small cost in terms of planning.
The only way to guarantee compatibility and reliable communication across a wide range of different computer systems is to define very strict standards that must be conformed to rigorously. That's exactly the approach taken by the planners of the Internet in determining its communications protocols. Please understand that I'm not the type of person who typically preaches conformity, but conformity in one's personal life is very different from conformity in complex computer networks.
The point is, the only way to allow a wide range of computer systems to coexist and communicate with each other effectively is to hammer out some standards. Fortunately, plenty of standards abound for the Internet, and they share wide support across many different computer systems. Hopefully, I've convinced you of the importance of communication standards on the Internet-let's take a look at a few of them.
One of the first areas of standardization on the Internet was
in establishing a means to uniquely identify each connected computer.
It's not surprising that a technique logically equivalent to traditional
mailing addresses is the one that was adopted; each computer physically
connected to the Internet is assigned an address that uniquely
identifies it. These addresses, also referred to as IP addresses,
come in the form of a 32-bit number that looks like this: 243.37.126.82.
You're probably more familiar with the symbolic form of IP addresses,
which looks like this: sams.mcp.com.
New Term |
An IP address is a 32-bit number that uniquely identifies each computer physically attached to the Internet. |
So addresses provide each computer connected to the Internet with a unique identifier. Each Internet computer has an address for the same reason you have a mailing address and a phone number at your home: to facilitate communication. It might sound simple, and that's because conceptually it is. As long as we can guarantee that each computer is uniquely identifiable, we can easily communicate with any computer without worry. Well, almost. The truth is, addresses are only a small part of the Internet communication equation, but an important part nevertheless. Without addresses, there would be no way to distinguish among different computers.
The idea of communicating among different computers on the Internet might not sound like that big a deal now that you understand that they use addresses similar to mailing addresses. The problem is that there are many different types of communication that can take place on the Internet, meaning that there must be an equal number of mechanisms for handling them. It's at this point that the mailing-address comparison to Internet addressing breaks down. The reason for this is that each type of communication taking place on the Internet requires a unique protocol. Your mailing-address essentially revolves around one type of communication: the postal carrier driving up to your mailbox and placing the mail inside.
A protocol specifies the format of data being sent over
the Internet, along with how and when it is sent. On the other
end of the communication, the protocol also defines how the data
is received along with its structure and what it means. You've
probably heard mention of the Internet being just a bunch of bits
flying back and forth in cyberspace. That's a very true statement,
and without protocols, those bits wouldn't mean anything.
New Term |
A protocol is a set of rules and standards defining a certain type of Internet communication. |
The concept of a protocol is not groundbreaking or even new. We use protocols all the time in everyday situations; we just don't call them protocols. Think about how many times you've been involved in this type of dialogue:
"Hi, may I take your order?"
"Yes, I'd like the grilled salmon and a frozen strawberry
margarita."
"Thanks, I'll put your order in and bring you your drink."
"Thank you, I'm famished."
Although this conversation might not look like anything special, it is a very definite social protocol used to place orders for food at a restaurant. Conversational protocol is important because it gives us familiarity and confidence in knowing what to do in certain situations. Haven't you ever been nervous when entering a new social situation in which you didn't quite know how to act? In these cases, you didn't really have confidence in the protocol, so you probably worried about a communication problem that could have easily resulted in embarrassment. For computers and networks, protocol breakdown translates into errors and information transfer failure rather than embarrassment.
Now that you understand the importance of protocols, let's take
a look at a couple of the more important ones used on the Internet.
Without a doubt, the protocol getting the most attention these
days is HTTP, which stands for Hypertext Transfer Protocol.
HTTP is the protocol used to transfer HTML documents on the Web.
Another important protocol is FTP, which stands for File
Transfer Protocol. FTP is a more general protocol used to transfer
binary files over the Internet. Each of these protocols has its
own unique set of rules and standards defining how information
is transferred, and Java provides support for both of them.
New Term |
HTTP stands for Hypertext Transfer Protocol, which is the protocol used to transfer HTML documents on the Web. |
New Term |
FTP stands for File Transfer Protocol, which is the protocol used to transfer files across the Internet. |
Internet protocols make sense only in the context of a service.
For example, the HTTP protocol comes into play when you are providing
Web content (HTML pages) through an HTTP service. Each computer
on the Internet has the capability to provide a variety of services
through the various protocols supported. There is a problem, however,
in that the type of service must be known before information can
be transferred. This is where ports come in. A port is
a software abstraction that provides a means to differentiate
between network services. More specifically, a port is a 16-bit
number identifying the different services offered by a network
server.
New Term |
A port is a 16-bit number that identifies each service offered by a network server. |
Each computer on the Internet has a bunch of ports that can be assigned different services. To use a particular service and therefore establish a line of communication via a particular protocol, you must connect to the correct port. Ports are numbered, and some of the numbers are specifically associated with a type of service. Ports with specific service assignments are known as standard ports, meaning that you can always count on a particular port corresponding to a certain service. For example, the FTP service is located on port 21, so any other computer wanting to perform an FTP file transfer would connect to port 21 of the host computer. Likewise, the HTTP service is located on port 80, so any time you access a Web site, you are really connecting to port 80 of the host using the HTTP protocol behind the scenes. Figure 26.1 illustrates how ports and protocols work.
Figure 26.1 : The relationship between protocols and ports.
All standard service assignments are given port values below 1024. This means that ports above 1024 are considered available for custom communications, such as those required by a Java client/server program implementing its own protocol. Keep in mind, however, that other types of custom communication also take place above port 1024, so you might have to try a few different ports to find an unused one.
So far I've managed to explain a decent amount of Internet networking fundamentals while dodging a major issue: the client/server paradigm. You've no doubt heard of clients and servers before, but you might not fully understand their importance in regard to the Internet. Well, it's time to remedy that situation, because you won't be able to get much done in Java without understanding how clients and servers work. As a matter of fact, the Java network- programming framework is based on a client/server arrangement.
The client/server paradigm involves thinking of computing in terms of a client, who is essentially in need of some type of information, and a server, who has lots of information and is just waiting to hand it out. Typically, a client will connect to a server and query for certain information. The server will go off and find the information and then return it to the client. It might sound as though I'm oversimplifying things here, but for the most part I'm not; conceptually, client/server computing is as simple as a client asking for information and a server returning it.
In the context of the Internet, clients are typically run on desktop or laptop computers attached to the Internet looking for information, whereas servers are typically run on larger computers with certain types of information available for the clients to retrieve. The Web itself is made up of a bunch of computers that act as Web servers; they have vast amounts of HTML pages and related data available for people to retrieve and browse. Web clients are used by those of us who connect to the Web servers and browse through the Web pages. In this way, Netscape Navigator is considered client Web software. Take a look at Figure 26.2 to get a better idea of the client/server arrangement.
Figure 26.2 : A Web server with multiple clients connected.
One of Java's major strong suits as a programming language is
its wide range of network support. Java has this advantage because
it was developed with the Internet in mind. The result is that
you have lots of options in regard to network programming in Java.
Even though there are many network options, most Java network
programming uses a particular type of network communication known
as sockets.
New Term |
A socket is a software abstraction for an input or output medium of communication. |
Java performs all of its low-level network communication through sockets. Logically, sockets are one step lower than ports; you use sockets to communicate through a particular port. So a socket is a communication channel that enables you to transfer data through a certain port. Check out Figure 26.3, which shows communication taking place through multiple sockets on a port.
Figure 26.3 : Multiple sockets communicating through a port.
This figure brings up an interesting point about sockets: Data can be transferred through multiple sockets for a single port. This makes sense because it is common for multiple Web users to retrieve Web pages from a server via port 80 (HTTP) at the same time. Java provides basic socket classes to make programming with sockets much easier. Java sockets are broken down into two types: datagram sockets and stream sockets.
A datagram socket uses User Datagram Protocol (UDP) to facilitate the sending of datagrams (self-contained pieces of information) in an unreliable manner. Unreliable means that information sent via datagrams isn't guaranteed to make it to its destination. The trade-off here is that datagram sockets require relatively few resources directly because of this unreliable design. The clients and servers in a datagram scenario don't require a "live" or dedicated network connection, which is sometimes desirable. In this way, a datagram socket is somewhat equivalent to a dial-up network connection, with which you are temporarily connected to a network based on your immediate information needs.
Datagrams are sent as individually bundled packets that may or
may not make it to their destination in any particular order or
at any particular time. On the receiving end of a datagram system,
the packets of information can be received in any order and at
any time. For this reason, datagrams sometimes include a sequence
number that specifies which piece of the puzzle each bundle corresponds
to. The receiver can then wait to receive the entire sequence,
in which case it will put them back together to form the original
information structure.
New Term |
UDP (User Datagram Protocol) is a network broadcast protocol that doesn't guarantee transfer success. In return, UDP relies on few network resources. |
New Term |
A datagram is an independent, self-contained piece of information sent over a network whose arrival, arrival time, and content are not guaranteed. |
New Term |
A datagram socket, or "unconnected" socket, is a socket over which data is bundled into packets and sent without requiring a "live" connection to the destination computer. |
The fact that datagram sockets are openly unreliable may lead you to think that they are something to avoid in network programming. However, there are very practical scenarios in which datagram sockets make perfectly acceptable solutions. For example, servers that continually broadcast similar information make great candidates for datagram communication. A stock quote server is a good example since the stock quotes are constantly being spit out with little regard for successful delivery. The fact that stock quotes are highly time-dependent makes it less of an issue if a stock quote never reaches you; you can just wait until a new one is sent.
Java supports datagram socket programming through two classes: DatagramSocket and DatagramPacket. The DatagramSocket class provides an implementation for a basic datagram socket. The DatagramPacket class provides the functionality required of a packet of information that is capable of being sent through a datagram socket. These two classes are all you need to get busy writing your own datagram client/server Java programs.
Following is a list of some of the more important methods implemented in the DatagramSocket class:
DatagramSocket()
DatagramSocket(int port)
void send(DatagramPacket p)
synchronized void receive(DatagramPacket p)
synchronized void close()
The first two methods listed are actually constructors for the DatagramSocket class. The first constructor is the default constructor and takes no parameters, and the second constructor creates a datagram socket connected to the specified port. The send and receive methods are very straightforward and provide a means to send and receive datagram packets. The close method simply closes the datagram socket. It doesn't get much simpler than that!
Notice that the DatagramSocket class doesn't distinguish between the socket being a client or server socket. The reason for this is the manner in which datagram communication takes place, which doesn't require that the socket act specifically as a client or server. Rather, Java clients and servers are distinguished by how they use the DatagramSocket class to transmit/receive datagrams.
The other half of the datagram solution is the DatagramPacket class, which models a packet of information sent through a datagram socket. Following are some of the more useful methods in the DatagramPacket class:
DatagramPacket(byte ibuf[], int ilength)
DatagramPacket(byte ibuf[], int ilength, InetAddress iaddr, int iport)
byte[] getData()
int getLength()
The first two methods are the constructors for DatagramPacket. As you probably guessed from the parameters, you construct datagram packets from byte arrays of data. The first constructor is used for receiving datagrams, as is evident by the absence of an address or port number. The second constructor is used for sending datagrams, which is why you have to specify a destination address and port number for the datagram to be sent. The other two methods return the raw datagram data and the length of the data, respectively.
Other than the constructors, all the methods in DatagramPacket are passive, meaning that they simply return information about the datagram packet and don't actually change anything. This is evidence that the DatagramPacket class is primarily used as a container for data being sent over a datagram socket. In other words, you will typically create a DatagramPacket object as a wrapper for data being sent or received and never call any methods on it.
Unlike datagram sockets, in which the communication is roughly
akin to that in a dial-up network, a stream socket is more akin
to a live network, in which the communication link is continuously
active. A stream socket is a "connected" socket through
which data is transferred continuously. By continuously,
I don't necessarily mean that data is being sent all the time,
but that the socket itself is active and ready for communication
all the time.
New Term |
A stream socket, or connected socket, is a socket through which data can be transmitted continuously. |
The benefit of using a stream socket is that information can be
sent with less worry about when it will arrive at its destination.
Because the communication link is always live, data is generally
transmitted immediately after you send it. Of course, this dedicated
communication link brings with it the overhead of consuming more
resources. However, most network programs benefit greatly from
the consistency and reliability of a stream socket.
Note |
A practical usage of a streaming mechanism is RealAudio, which is a technology that provides a way to listen to audio on the Web as it is being transmitted in real time. |
Java supports stream socket programming primarily through two classes: Socket and ServerSocket. The Socket class provides the necessary overhead to facilitate a stream socket client, and the ServerSocket class provides the core functionality for a server.
Following is a list of some of the more important methods implemented in the Socket class:
Socket(String host, int port)
Socket(InetAddress address, int port)
synchronized void close()
InputStream getInputStream()
OutputStream getOutputStream()
The first two methods listed are constructors for the Socket class. The host computer you are connecting the socket to is specified in the first parameter of each constructor; the difference between the two constructors is whether you specify the host using a string name or an InetAddress object. The second parameter is an integer specifying the port you want to connect to. The close method is used to close a socket. The getInputStream and getOutputStream methods are used to retrieve the input and output streams associated with the socket.
The ServerSocket class handles the other end of socket communication in a client/server scenario. Following are a few of the more useful methods defined in the ServerSocket class:
ServerSocket(int port)
ServerSocket(int port, int count)
Socket accept()
void close()
The first two methods are the constructors for ServerSocket, which both take a port number as the first parameter. The count parameter in the second constructor specifies a timeout period for the server to quit automatically "listening" for a client connection. This is the distinguishing factor between the two constructors; the first version doesn't listen for a client connection, whereas the second version does. If you use the first constructor, you must specifically tell the server to wait for a client connection. You do this by calling the accept method, which blocks program flow until a connection is made. The close method simply closes the server socket.
Like with the datagram socket classes, you might be thinking that the stream socket classes seem awfully simple. In fact, they are simple, which is a good thing. Most of the actual code facilitating communication via stream sockets is handled through the input and output streams connected to a socket. In this way, the communication itself is handled independently of the network socket connection. This might not seem like a big deal at first, but it is crucial in the design of the socket classes; after you've created a socket, you connect an input or output stream to it and then forget about the socket.
You've now covered the basics of sockets and how they work in Java, but you haven't seen a socket in action. Well, it's time to remedy that situation with a full-blown client/server program that uses datagram sockets. You'll also work through a stream socket example later today, but first things first!
The datagram client/server example is called Fortune and consists of a server that transmits interesting quotes called "fortunes" and a client that receives and displays the fortunes. The Fortune example could also be used to implement a joke-of-the-day server, where users can connect and get the latest joke you have to offer. Since I had more interesting quotes than funny jokes, I decided to stick with a quote server!
The Fortune example works like this: There is a server program that runs on a Web server and waits patiently for clients to connect and ask for a fortune. On the other end, there is a client applet embedded in a Web page that a user accesses with a Java-enabled Web browser. When the user loads the Web page and fires up the applet, the applet connects to the server and asks it for a fortune. The server in turn picks a fortune at random and sends it back to the applet. The applet in return displays the fortune for the user to see. It's that simple!
Before jumping into the Java code required to implement the Fortune example, let's briefly take a look at what is required of the design on each side of the client/server fence. On the server side, you need a program that monitors a particular port on the host machine for client connections. When a client is detected, the server picks a random fortune, which is a simple text string, and sends it to the client over the specified port. The server is then free to break the connection and let the client go on its merry way. The server returns to its original wait state, where it looks for other clients to connect. So the server is required to perform the following tasks:
Now, on to the client. The client side of the Fortune example is an applet that lives in a Web page and has full support for graphical output. The client applet is responsible for connecting to the server and awaiting the server's response. The server's response is the transmission of the fortune string, which the client must receive and display. When the client successfully receives the fortune, it can break the connection with the server. As an added bonus, the client applet is also capable of grabbing another fortune if you click the mouse button. The client's primary tasks follow:
You're no doubt itching to see some real code that carries out all these ideas you've been learning. Well, the time has come! Since the Fortune example ultimately begins and ends with the server, let's start by looking at the code for the server. The complete source code for the FortuneServer class is located on the accompanying CD-ROM in the file FortuneServer.java. Following are the member variables defined in the FortuneServer class:
private static final int PORTNUM = 1234; private String[] fortunes; private DatagramSocket serverSocket; private Random rand = new Random(System.currentTimeMillis());
The PORTNUM member represents
the number of the port used by Fortune. The value of PORTNUM-1234-is
arbitrarily chosen; the important thing is that it is greater
than 1024. The fortunes member
variable is an array of strings that hold the text for the actual
fortunes. The serverSocket
member variable represents the datagram socket used for communication
with the client. The rand
member variable is a Random
object that is used in determining the random fortune to be sent
to the client.
Warning |
Be sure to always make your port numbers greater than 1024 so that they don't conflict with standard server port assignments. |
The constructor for FortuneServer handles creating the server socket:
public FortuneServer() { super("FortuneServer"); try { serverSocket = new DatagramSocket(PORTNUM); System.out.println("FortuneServer up and running..."); } catch (SocketException e) { System.err.println("Exception: couldn't create datagram socket"); System.exit(1); } }
As you can see, the constructor creates a datagram socket using the port number specified by PORTNUM. If the socket cannot be created, an exception is thrown, and the server exits. The server exits because it is pretty much worthless without a socket to communicate through.
The method that does most of the work in the FortuneServer class is the run method, which is shown in Listing 26.1.
Listing 26.1. The run method.
1: public void run() { 2: if (serverSocket == null) 3: return; 4: 5: // Initialize the array of fortunes 6: if (!initFortunes()) { 7: System.err.println("Error: couldn't initialize fortunes"); 8: return; 9: } 10: 11: // Look for clients and serve up the fortunes 12: while (true) { 13: try { 14: InetAddress address; 15: int port; 16: DatagramPacket packet; 17: byte[] data = new byte[256]; 18: int num = Math.abs(rand.nextInt()) % fortunes.length; 19: 20: // Wait for a client connection 21: packet = new DatagramPacket(data, data.length); 22: serverSocket.receive(packet); 23: 24: // Send a fortune 25: address = packet.getAddress(); 26: port = packet.getPort(); 27: fortunes[num].getBytes(0, fortunes[num].length(), data, 0); 28: packet = new DatagramPacket(data, data.length, address, port); 29: serverSocket.send(packet); 30: } 31: catch (Exception e) { 32: System.err.println("Exception: " + e); 33: e.printStackTrace(); 34: } 35: } 36: }
Analysis |
The first thing the run method does is check to make sure the socket is valid. If the socket is okay, run calls initFortunes to initialize the array of fortune strings. You'll learn about the initFortunes method in just a moment. Once the fortunes are initialized, run enters an infinite while loop that waits for a client connection. When a client connection is detected, a datagram packet is created using a random fortune string. This packet is then sent to the client through the socket. |
Since you wouldn't want to have to recompile the server application every time you wanted to change the fortunes, the fortunes are read from a text file. Each fortune is stored as a single line of text in the file Fortunes.txt. Following is a listing of the Fortunes.txt file:
You can no more win a war than you can win an earthquake. The highest result of education is tolerance. The right to be let alone is indeed the beginning of all freedom. When we lose the right to be different, we lost the right to be free. The only vice that cannot be forgiven is hypocrisy. We learn from history that we do not learn from history. That which we call sin in others is experiment for us. Few men have virtue to withstand the highest bidder.
The initFortunes method is
responsible for reading the fortunes from this file and
storing them into an array that is more readily accessible. Listing
26.2 contains the source code for the initFortunes
method.
Listing 26.2. The initFortunes method.
1: private boolean initFortunes() { 2: try { 3: File inFile = new File("Fortunes.txt"); 4: FileInputStream inStream = new FileInputStream(inFile); 5: byte[] data = new byte[(int)inFile.length()]; 6: 7: // Read the fortunes into a byte array 8: if (inStream.read(data) <= 0) { 9: System.err.println("Error: couldn't read fortunes"); 10: return false; 11: } 12: 13: // See how many fortunes there are 14: int numFortunes = 0; 15: for (int i = 0; i < data.length; i++) 16: if (data[i] == (byte)'\n') 17: numFortunes++; 18: fortunes = new String[numFortunes]; 19: 20: // Parse the fortunes into an array of strings 21: int start = 0, index = 0; 22: for (int i = 0; i < data.length; i++) 23: if (data[i] == (byte)'\n') { 24: fortunes[index++] = new String(data, 0, start, i - start - 1); 25: start = i + 1; 26: } 27: } 28: catch (FileNotFoundException e) { 29: System.err.println("Exception: couldn't find the fortune file"); 30: return false; 31: } 32: catch (IOException e) { 33: System.err.println("Exception: I/O error trying to read fortunes"); 34: return false; 35: } 36: 37: return true; 38: }
Analysis |
The initFortunes method first creates a File object based on the Fortunes.txt file, which is used to initialize a file input stream. The File object is also used to determine the length of the fortunes file. The length of the file is important because it is used to create a byte array large enough to hold all the fortunes that are read. |
initFortunes reads the fortunes from the text file with a simple call to the read method of the input stream. The number of fortunes is then determined by counting the number of newline characters ('\n') in the fortune text. This works because each fortune is separated by a newline character in the file. When the number of fortunes has been established, a string array is created that is large enough to hold the fortunes. Using newline characters as separators, the fortune text is then parsed and each fortune stored in the array of strings. The end result is an array of strings that is much more convenient to access than attempting to read a file every time a client wants a fortune.
The last method in the FortuneServer class is main, which is the entry point of the server application:
public static void main(String[] args) { FortuneServer server = new FortuneServer(); server.start(); }
As you can see, the main method is very simple; it creates a FortuneServer object and tells it to start running. That's it for the server side of Fortune!
You might have been surprised by the simplicity of the Fortune server code. If so, then you'll probably be even more surprised by the client side of Fortune. The Fortune client class is simply called Fortune and is located on the CD-ROM in the file Fortune.java. Following are the member variables defined in the Fortune class:
private static final int PORTNUM = 1234; private String fortune;
The PORTNUM member should
be very familiar to you. Notice that it is set to the same value
as the PORTNUM variable defined
in FortuneServer. This is
critical because the port number is what ties the two programs
together. The fortune member
variable simply holds the current fortune being displayed.
Warning |
It is very important that the port numbers for your client and server match exactly, because the port number is how the client and server are linked to each other. |
The Fortune applet attempts to grab a fortune from the server as soon as it runs. This is accomplished in the init method, whose code follows:
public void init() { fortune = getFortune(); if (fortune == null) fortune = "Error: No fortunes found!"; }
The init method calls getFortune to get a fortune from the server. If the fortune is invalid, an error message is displayed instead. The getFortune method handles the work of actually connecting to and getting a fortune from the server. The code for getFortune is shown in Listing 26.3.
Listing 26.3. The getFortune method.
1: private String getFortune() { 2: try { 3: DatagramSocket socket; 4: DatagramPacket packet; 5: byte[] data = new byte[256]; 6: 7: // Send a fortune request to the server 8: socket = new DatagramSocket(); 9: packet = new DatagramPacket(data, data.length, 10: InetAddress.getByName(getCodeBase().getHost()), PORTNUM); 11: socket.send(packet); 12: 13: // Receive a fortune 14: packet = new DatagramPacket(data, data.length); 15: socket.receive(packet); 16: fortune = new String(packet.getData(), 0); 17: socket.close(); 18: } 19: catch (UnknownHostException e) { 20: System.err.println("Exception: host could not be found"); 21: return null; 22: } 23: catch (Exception e) { 24: System.err.println("Exception: " + e); 25: e.printStackTrace(); 26: return null; 27: } 28: return fortune; 29: }
Analysis |
The getFortune method first creates a request packet and sends it to the server. The contents of this packet are unimportant; the point is to just make contact with the server. After sending the request packet, getFortune creates a new packet and uses it to receive a fortune from the server. |
Because Fortune is an applet, the fortunes are displayed graphically via the paint method. Listing 26.4 contains the paint method defined in the Fortune class.
Listing 26.4. The paint method.
1: public void paint(Graphics g) { 2: // Draw the title and fortune text 3: Font f1 = new Font("TimesRoman", Font.BOLD, 28), 4: f2 = new Font("Helvetica", Font.PLAIN, 16); 5: FontMetrics fm1 = g.getFontMetrics(f1), 6: fm2 = g.getFontMetrics(f2); 7: String title = new String("Today's Fortune:"); 8: g.setFont(f1); 9: g.drawString(title, (size().width - fm1.stringWidth(title)) / 2, 10: ((size().height - fm1.getHeight()) / 2) + fm1.getAscent()); 11: g.setFont(f2); 12: g.drawString(fortune, (size().width - fm2.stringWidth(fortune)) / 2, 13: size().height - fm2.getHeight() - fm2.getAscent()); 14: }
Analysis |
The paint method may look a little complicated, but all it's doing is performing some fancy centering and alignment so that the positioning of the fortune looks good. The paint method also displays the text Today's Fortune: just above the fortune. |
The final aspect of the Fortune class that you haven't covered is how to get a new fortune when the user clicks the mouse button. This is handled in the mouseDown method, whose code follows:
public boolean mouseDown(Event evt, int x, int y) { // Display a new fortune getFortune(); repaint(); return true; }
Since getFortune already takes care of the details involved in getting a new fortune from the server, all the mouseDown method has to do is call getFortune and update the screen with a call to repaint. That sums up the client side of Fortune, which means you're probably ready to take it for a spin!
As you already know, the Fortune example is composed of two parts: a client and a server. The Fortune server must be running in order for the client to work. So to get things started, you must first run the server by using the Java interpreter (java); you do this from a command line, like this:
java FortuneServer
The other half of Fortune is the client, which is an applet that runs from within a Java-compatible Web browser, like Netscape Navigator or Microsoft Internet Explorer. After you have the server up and running, fire up a browser and load an HTML document including the Fortune client applet. On the CD-ROM, this HTML document is called Example1.html, in keeping with the standard JDK demo applets. After running the Fortune client applet, you should see something similar to what's shown in Figure 26.4. You can click in the applet window to retrieve new fortunes.
Figure 26.4 : The Fortune client applet.
Note |
This discussion on running the Fortune example assumes that you either have access to a Web server or can simulate a network connection on your local machine. Since my local Windows 95 system is not part of a physical network, I tested the programs by simulating a network connection. I did this by changing the TCP/IP configuration on my system so that it used a specific IP address (I just made up an address). If you make this change to your network configuration, you won't be able to access a real network using TCP/IP until you set it back, so don't forget to restore things when you're finished. |
The Fortune programs are a good example of how to use Java's datagram networking facilities. You will probably find, however, that more networking problems require a stream approach. Since I wouldn't want to leave you feeling like half a Java network programmer, let's look at an example that requires a stream socket approach.
The stream client/server example is called Trivia and consists of a server that asks trivia questions and a client that interacts with the server by allowing the user to answer the questions. The Trivia example differs from the Fortune example in that there is an ongoing two-way communication between the client and the server.
The Trivia example works like this: The server program waits patiently for a client to connect. When a client connects, the server sends a question and waits for a response. On the other end, the client receives the question and prompts the user for an answer. The user types in an answer that is sent back to the server. The server then checks to see if the answer is correct and notifies the user. The server follows this up by asking the client if it wants another question. If so, the process repeats.
It's important to always perform a brief preliminary design before you start churning out code. With that in mind, let's take a look at what is required of the Trivia server and client. On the server side, you need a program that monitors a particular port on the host machine for client connections, just as you did in Fortune. When a client is detected, the server picks a random question and sends it to the client over the specified port. The server then enters a wait state until it hears back from the client. When it gets an answer back from the client, the server checks it and notifies the client whether it is correct or incorrect. The server then asks the client if it wants another question, upon which it enters another wait state until the client answers. Finally, the server either repeats the process by asking another question, or it terminates the connection with the client. In summary, the server performs the following tasks:
Unlike Fortune, the client side of the Trivia example is an application that runs from a command line. The client is responsible for connecting to the server and waiting for a question. When it receives a question from the server, the client displays it to the user and allows the user to type in an answer. This answer is sent back to the server, and the client again waits for the server's response. The client displays the server's response to the user and allows the user to confirm whether he wants another question. The client then sends the user's response to the server and exits if the user declined any more questions. The client's primary tasks follow:
Like Fortune, the heart of the Trivia example lies in the server. The Trivia server program is called TriviaServer and is located on the CD-ROM in the file TriviaServer.java. Following are the member variables defined in the TriviaServer class:
private static final int PORTNUM = 1234;
private static final int WAITFORCLIENT = 0;
private static final int WAITFORANSWER = 1;
private static final int WAITFORCONFIRM = 2;
private String[] questions;
private String[] answers;
private ServerSocket serverSocket;
private int numQuestions;
private int num = 0;
private int state = WAITFORCLIENT;
private Random rand = new Random(System.currentTimeMillis());
The WAITFORCLIENT, WAITFORANSWER, and WAITFORCONFIRM members are all state constants that define different states the server can be in. You'll see these constants in action in a moment. The questions and answers member variables are string arrays used to store the questions and corresponding answers. The serverSocket member variable keeps up with the server socket connection. numQuestions is used to store the total number of questions, while num is the number of the current question being asked. The state member variable holds the current state of the server, as defined by the three state constants (WAITFORCLIENT, WAITFORANSWER, and WAITFORCONFIRM). Finally, the rand member variable is used to pick questions at random.
The TriviaServer constructor is very similar to FortuneServer's constructor, except that it creates a ServerSocket rather than a DatagramSocket. Check it out:
public TriviaServer() { super("TriviaServer"); try { serverSocket = new ServerSocket(PORTNUM); System.out.println("TriviaServer up and running..."); } catch (IOException e) { System.err.println("Exception: couldn't create socket"); System.exit(1); } }
Also like Fortune, the run method in TriviaServer is where most of the action is. The source code for the run method is shown in Listing 26.5.
Listing 26.5. The run method.
1: public void run() { 2: Socket clientSocket; 3: 4: // Initialize the arrays of questions and answers 5: if (!initQnA()) { 6: System.err.println("Error: couldn't initialize questions and answers"); 7: return; 8: } 9: 10: // Look for clients and ask trivia questions 11: while (true) { 12: // Wait for a client 13: if (serverSocket == null) 14: return; 15: try { 16: clientSocket = serverSocket.accept(); 17: } 18: catch (IOException e) { 19: System.err.println("Exception: couldn't connect to client socket"); 20: System.exit(1); 21: } 22: 23: // Perform the question/answer processing 24: try { 25: DataInputStream is = new DataInputStream(new 26: BufferedInputStream(clientSocket.getInputStream())); 27: PrintStream os = new PrintStream(new 28: BufferedOutputStream(clientSocket.getOutputStream()), false); 29: String inLine, outLine; 30: 31: // Output server request 32: outLine = processInput(null); 33: os.println(outLine); 34: os.flush(); 35: 36: // Process and output user input 37: while ((inLine = is.readLine()) != null) { 38: outLine = processInput(inLine); 39: os.println(outLine); 40: os.flush(); 41: if (outLine.equals("Bye.")) 42: break; 43: } 44: 45: // Cleanup 46: os.close(); 47: is.close(); 48: clientSocket.close(); 49: } 50: catch (Exception e) { 51: System.err.println("Exception: " + e); 52: e.printStackTrace(); 53: } 54: } 55: }
Analysis |
The run method first initializes the questions and answers by calling initQnA. You'll learn about the initQnA method in a moment. An infinite while loop is then entered that waits for a client connection. When a client connects, the appropriate I/O streams are created, and the communication is handled via the processInput method. You'll learn about processInp ut next. processInput continually processes client responses and handles asking new questions until the client decides not to receive any more questions. This is evidenced by the server sending the string "Bye.". The run method then cleans up the streams and client socket. |
The processInput method keeps up with the server state and manages the logic of the whole question/answer process. The source code for processInput is shown in Listing 26.6.
Listing 26.6. The processInput method.
1: String processInput(String inStr) { 2: String outStr; 3: 4: switch (state) { 5: case WAITFORCLIENT: 6: // Ask a question 7: outStr = questions[num]; 8: state = WAITFORANSWER; 9: break; 10: 11: case WAITFORANSWER: 12: // Check the answer 13: if (inStr.equalsIgnoreCase(answers[num])) 14: outStr = "That's correct! Want another? (y/n)"; 15: else 16: outStr = "Wrong, the correct answer is " + answers[num] + 17: ". Want another? (y/n)"; 18: state = WAITFORCONFIRM; 19: break; 20: 21: case WAITFORCONFIRM: 22: // See if they want another question 23: if (inStr.equalsIgnoreCase("y")) { 24: num = Math.abs(rand.nextInt()) % questions.length; 25: outStr = questions[num]; 26: state = WAITFORANSWER; 27: } 28: else { 29: outStr = "Bye."; 30: state = WAITFORCLIENT; 31: } 32: break; 33: } 34: return outStr; 35: }
Analysis |
The first thing to note about the processInput method is the outStr local variable. The value of this string is sent back to the client in the run method when processInput returns. So keep an eye on how processInput uses outStr to convey information back to the client. |
In FortuneServer, the state WAITFORCLIENT represents the server when it is idle and waiting for a client connection. Understand that each case statement in processInput() represents the server leaving the given state. For example, the WAITFORCLIENT case statement is entered when the server has just left the WAITFORCLIENT state. In other words, a client has just connected to the server. When this occurs, the server sets the output string to the current question and sets the state to WAITFORANSWER.
If the server is leaving the WAITFORANSWER state, it means that the client has responded with an answer. processInput checks the client's answer against the correct answer and sets the output string accordingly. It then sets the state to WAITFORCONFIRM.
The WAITFORCONFIRM state represents the server waiting for a confirmation answer from the client. In processInput, the WAITFORCONFIRM case statement indicates that the server is leaving the state because the client has returned a confirmation (yes or no). If the client answered yes with a y, processInput picks a new question and sets the state back to WAITFORANSWER. Otherwise, the server tells the client Bye. and returns the state to WAITFORCLIENT to await a new client connection.
Similar to Fortune, the questions and answers in Trivia are stored in a text file. This file is called QnA.txt and is organized with questions and answers on alternating lines. By alternating, I mean that each question is followed by its answer on the following line, which is in turn followed by the next question. Following is a partial listing of the QnA.txt file:
What caused the craters on the moon? meteorites How far away is the moon (in miles)? 239000 How far away is the sun (in millions of miles)? 93 Is the Earth a perfect spere? no What is the internal temperature of the Earth (in degrees F)? 9000
The initQnA method handles the work of reading the questions and answers from the text file and storing them in separate string arrays. Listing 26.7 contains the source code for the initQnA method.
Listing 26.7. The initQnA method.
1: private boolean initQnA() { 2: try { 3: File inFile = new File("QnA.txt"); 4: FileInputStream inStream = new FileInputStream(inFile); 5: byte[] data = new byte[(int)inFile.length()]; 6: 7: // Read the questions and answers into a byte array 8: if (inStream.read(data) <= 0) { 9: System.err.println("Error: couldn't read questions and answers"); 10: return false; 11: } 12: 13: // See how many question/answer pairs there are 14: for (int i = 0; i < data.length; i++) 15: if (data[i] == (byte)'\n') 16: numQuestions++; 17: numQuestions /= 2; 18: questions = new String[numQuestions]; 19: answers = new String[numQuestions]; 20: 21: // Parse the questions and answers into arrays of strings 22: int start = 0, index = 0; 23: boolean isQ = true; 24: for (int i = 0; i < data.length; i++) 25: if (data[i] == (byte)'\n') { 26: if (isQ) { 27: questions[index] = new String(data, 0, start, i - start - 1); 28: isQ = false; 29: } 30: else { 31: answers[index] = new String(data, 0, start, i - start - 1); 32: isQ = true; 33: index++; 34: } 35: start = i + 1; 36: } 37: } 38: catch (FileNotFoundException e) { 39: System.err.println("Exception: couldn't find the fortune file"); 40: return false; 41: } 42: catch (IOException e) { 43: System.err.println("Exception: I/O error trying to read questions"); 44: return false; 45: } 46: 47: return true; 48: }
Analysis |
The initQnA method is similar to the initFortunes method in FortuneServer, except that in this case two arrays are being filled with alternating strings. The two arrays are the question and answer string arrays. Rather than repeat the earlier explanation for initFortunes, I'll leave it up to you to compare and contrast the differences between initFortunes and initQnA. You'll find that the differences are very small and have to do with the fact that you are now filling two arrays with alternating strings. |
The only remaining method in TriviaServer is main, which follows:
public static void main(String[] args) { TriviaServer server = new TriviaServer(); server.start(); }
Like the main method in FortuneServer, all this main method does is create the server object and get it started with a call to the start method.
Since the client side of the Trivia example requires the user to type in answers and receive responses back from the server, it is more straightforward to implement as a command-line application. Sure, this may not be as cute as a graphical applet, but it makes it very easy to see the communication events as they unfold. The client application is called Trivia and is located on the CD-ROM in the file Trivia.java.
The only member defined in the Trivia class is PORTNUM, which defines the port number used by both the client and server. There is also only one method defined in the Trivia class: main. The source code for the main method is shown in Listing 26.8.
Listing 26.8. The main method.
1: public static void main(String[] args) { 2: Socket socket; 3: DataInputStream in; 4: PrintStream out; 5: String address; 6: 7: // Check the command-line args for the host address 8: if (args.length != 1) { 9: System.out.println("Usage: java Trivia <address>"); 10: return; 11: } 12: else 13: address = args[0]; 14: 15: // Initialize the socket and streams 16: try { 17: socket = new Socket(address, PORTNUM); 18: in = new DataInputStream(socket.getInputStream()); 19: out = new PrintStream(socket.getOutputStream()); 20: } 21: catch (IOException e) { 22: System.err.println("Exception: couldn't create stream socket"); 23: System.exit(1); 24: } 25: 26: // Process user input and server responses 27: try { 28: StringBuffer str = new StringBuffer(128); 29: String inStr; 30: int c; 31: 32: while ((inStr = in.readLine()) != null) { 33: System.out.println("Server: " + inStr); 34: if (inStr.equals("Bye.")) 35: break; 36: while ((c = System.in.read()) != '\n') 37: str.append((char)c); 38: System.out.println("Client: " + str); 39: out.println(str.toString()); 40: out.flush(); 41: str.setl1ength(0); 42: } 43: 44: // Cleanup 45: out.close(); 46: in.close(); 47: socket.close(); 48: } 49: catch (IOException e) { 50: System.err.println("Exception: I/O error trying to talk to server"); 51: } 52: }
Analysis |
The first interesting thing you might notice about the main method is that it looks for a command-line argument. The command-line argument required of the Trivia client is the address of the server, such as thetribe.com. You may be wondering why the Fortune client didn't require you to specify a server address. The reason is that Java applets are accessed via Web pages, which are always associated with a particular server. So Java applets are inherently tied to a server and can therefore query the server for its address. This was accomplished in the Fortune client by calling the getHost method. |
With Java applications, you don't have this option because there is no inherent server associated with the application. So you have to either hard-code the server address or ask for it as a command-line argument. I'm not very fond of hard-coding because it requires you to recompile any time you want to change something. Hence the command-line argument!
If the server address command-line argument is valid (not null), the main method creates the necessary socket and I/O streams. It then enters a while loop, where it processes information from the server and transmits user requests back to the server. When the server quits sending information, the while loop falls through, and the main method cleans up the socket and streams. And that's all there is to the Trivia client!
Like Fortune, the Trivia server must be running in order for the client to work. To get things started, you must first run the server by using the Java interpreter; this is done from a command line, like this:
java TriviaServer
The Trivia client is also run from a command line, but you must specify a server address as the only argument. Following is an example of running the Trivia client and connecting to the server thetribe.com:
java Trivia "thetribe.com"
After running the Trivia client and answering a few questions, you should see output similar to this:
Server: Is the Galaxy rotating? yes Client: yes Server: That's correct! Want another? (y/n) y Client: y Server: Is the Earth a perfect sphere? no Client: no Server: That's correct! Want another? (y/n) y Client: y Server: What caused the craters on the moon? asteroids Client: asteroids Server: Wrong, the correct answer is meteorites. Want another? (y/n) n Client: n Server: Bye.
Today you have learned a wealth of information about client/server network programming in Java. You began the lesson by learning some fundamental concepts about the Internet and how it is organized as a network. More specifically, you learned about addresses, protocols, and ports, which all play a critical role in Internet communications. From there, you moved on to learning about client/server computing and how Java supports the client/server model through two different types of sockets: datagram sockets and stream sockets.
The last half of today's lesson led you through building two complete client/server programs. These two examples demonstrate the differing approaches to client/server network programming afforded by the Java datagram and stream socket classes. Both of these examples should serve as a solid basis for your own client/server projects.
If all the coding over the past few days has taken its toll on you, relax-tomorrow's lesson involves absolutely no programming. Tomorrow's lesson covers the Java standard extension APIs, which are a new set of API extensions that promise to add all kinds of neat features to Java. Aren't you excited?
Why is the client/server paradigm so important in Java network programming? | |
The client/server model was integrated into Java because it has proved time and again to be superior to other networking approaches. By dividing the process of serving data from the process of viewing and working with data, the client/server approach provides network developers with the freedom to implement a wide range of solutions to common network problems. | |
Why are datagram sockets less suitable for network communications than stream sockets? | |
The primary reason is speed, because you have no way of knowing when information transferred through a datagram socket will reach its destination. Admittedly, you don't really know for sure when stream socket data will get to its destination either, but you can rest assured it will be faster than with the datagram socket. Also, datagram socket transfers have the additional complexity of your having to reorganize the incoming data, which is an unnecessary and time-consuming annoyance except in very rare circumstances. | |
How do I incorporate Fortune into a Web site? | |
Beyond simply including the client applet in an HTML document that is served up by your Web server, you must also make sure that the Fortune server (FortuneServer) is running on the Web server machine. Without the fortune server, the clients are worthless. | |
How do I change the trivia questions and answers for Trivia? | |
You simply edit the QnA.txt text file and add as many questions and answers as you want. Just make sure that each question and answer appears on its own line, and that each answer immediately follows its corresponding question. |