In this chapter you will learn how to write client programs that support networked client/server applications. You'll learn about the typical client programs found on the Internet and how they are structured. You'll develop simple client programs that support remote login and the sending of mail, and that fetch a list of Web pages. This chapter builds on the material presented in Chapter 17, "Network Programming with the java.net Package." You might want to review Chapter 17 before continuing with the material presented in this chapter.
Of the client/server applications that are found on the Internet, only a small group is typically used. These include e-mail, the Web, FTP, Usenet news groups, and telnet. Gopher and WAIS, both precursors of the Web, have declined in popularity, having been subsumed by the Web. Typical Internet client programs include e-mail programs, Web browsers, FTP programs, and telnet clients.
E-mail programs provide an easy-to-use interface by which mail can be created and sent, retrieved and displayed, and managed. Popular window-based clients include Eudora and Pegasus. UNIX systems provide a number of popular e-mail clients, including Pine, Elm, and mh.
Web browsers provide a window on the World Wide Web and support the display of Web pages, including, of course, Java programs. The Netscape browser is the most popular browser on the Web and is Java capable. It is supported on UNIX, Windows, and Macintosh systems.
FTP programs provide a convenient way to retrieve files from public Internet file servers or from private file directories. Although a number of user-friendly FTP client programs are available, the simple text-based FTP client is still the most popular and most widely supported.
Newsreader programs simplify the process of working with messages that are posted to Usenet news groups. A number of netnews client programs are available for Windows, Macintosh, UNIX, and other operating system platforms.
Telnet clients are used to remotely log into other systems. These systems are usually UNIX or other operating systems that are powerful enough to provide the underlying capabilities needed to implement multiuser support. Windows and Macintosh systems, because of their inherent limitations, do not support telnet server applications.
Client programs perform a service for their users by connecting with their server counterparts, forwarding service requests based on user inputs, and providing the service results back to the user.
In most cases, the client must initiate the connection. Typically, the server listens on a well-known port for a client connection. The client initiates the connection, which is accepted by the server. The client sends a service request to the server, based on user inputs. The server receives the service request, performs the service, and returns the results of the service to the client. The client receives the service results and displays them to the user.
A telnet client program provides users with the capability to log into remote systems. It connects to a telnet server (called a telnet daemon) that listens on port 23 for an incoming connection. The telnet client connects to the daemon, which usually runs a login program and, upon successful login, runs a shell program.
The telnet client must be capable of simultaneously exchanging data with both the user and the remote system. The protocol used for communication between the client and the server is specified in RFC 854, the Telnet Protocol Specification. RFC 854 identifies three basic elements of the telnet protocol: the concept of a network virtual terminal, the principle of negotiated options, and the symmetry between terminals and processes.
The network virtual terminal, or NVT, is a very simple device that forms the basis for establishing telnet-based communication. All telnet clients and servers are required to support the NVT as a minimum capability. It is an abstract device that consists of a printer and a keyboard. The user types characters on the keyboard that are forwarded to the server. The server returns data to the user and the NVT displays it on the printer. The NVT provides local character echoing and half-duplex operation, although remote echoing and full-duplex operation can be used as negotiated options. Lines are terminated using a standard carriage-return-line-feed combination.
The NVT also provides for control operations that support process interruption and the discarding of excessive output. These operations are signaled by using the Interpret as Command (IAC) code as described in the next section.
The IAC code is sent from a client or server to a program on the other end of a telnet connection to send a control code or to negotiate an option, as described in the next section. The IAC is a single byte consisting of the value 255 or hex 0xFF. The IAC may be followed by a single byte to send a control code, or by two or more bytes to negotiate an option. For example, the IAC followed by a byte with the decimal value of 243 is used to send a break command.
Because the IAC is used to indicate a command or option negotiated, a special byte sequence is needed to send the byte value 255 used for the IAC. This is accomplished by sending two IACs in succession.
Because all telnet clients and servers are required to implement the NVT, they all have a common, but primitive, basis from which to begin operation. Additional options, such as full- duplex operation and character echoing, can be used based on the principle of negotiated options.
Options are negotiated when either the client or server program sends an IAC code to the other. The IAC code is followed by a WILL or DO code and an option code. The WILL code informs the program on the other side of the connection that it intends to use a particular option. The other program may respond with a DO or a DONT response, consisting of the IAC, followed by the DO or DONT code, followed by the option.
A program can also request that the program on the other side of the connection implement an option. This is accomplished by sending the IAC code, the DO code, and the option code. The other program can respond with a WILL or WONT response. A WILL response is indicated by sending the IAC, followed by the WILL code, followed by the option code. A WONT response is sent in the same manner, with the WONT code being used instead of the WILL code.
As you probably have surmised from reading the previous sections, the communication between client and server is highly symmetrical. Either the client or server can initiate option negotiation. The use of symmetry between client and host simplifies the implementation of the telnet protocol and allows client and host software to be developed from a common base. The TelnetApp program, presented in the next section, makes use of two I/O filters, NVTInputStream and NVTOutputStream, that implement some of the basic elements of the telnet protocol. These streams do not support control characters or additional options. Option negotiation is handled by refusing any additional options other than those provided by the basic NVT.
The TelnetApp program implements a minimum set of features of the telnet protocol in order to accomplish a remote login to a telnet server. The purpose of the program is not to provide you with a telnet client, but to show you the basics of how these clients work. More sophisticated and powerful telnet client programs can be retrieved from the Internet. The source code of the TelnetApp program is shown in Listing 26.1.
Listing 26.1. The source code for the TelnetApp program.
import java.lang.*;
import
java.net.*;
import
java.io.*;
import
jdg.ch26.NVTInputStream;
import
jdg.ch26.NVTOutputStream;
import
jdg.ch26.NVTPrinter;
public
class TelnetApp {
public
static void main(String args[]){
PortTalk
portTalk = new PortTalk(args);
portTalk.start();
}
}
class
PortTalk extends Thread {
Socket
connection;
PrintStream
outStream;
NVTInputStream
inStream;
NVTPrinter
printer;
public
PortTalk(String args[]){
if(args.length!=2)
error("Usage: java TelnetApp host port");
String
destination = args[0];
int
port = 0;
try
{
port
= Integer.valueOf(args[1]).intValue();
}catch
(NumberFormatException ex) { error("Invalid port number"); }
try{
connection
= new Socket(destination,port);
}catch
(UnknownHostException ex) { error("Unknown host"); }
catch
(IOException ex) { error("IO error creating socket"); }
try{
outStream
= new PrintStream(connection.getOutputStream());
inStream
= new NVTInputStream(connection.getInputStream(),outStream);
}catch
(IOException ex) { error("IO error getting streams"); }
System.out.println("Connected
to "+destination+" at port "+port+".");
}
public
void run() {
printer
= new NVTPrinter(inStream);
printer.start();
yield();
processUserInput();
shutdown();
}
public
void processUserInput() {
try
{
String
line;
boolean
finished = false;
DataInputStream
userInputStream = new DataInputStream(System.in);
do
{
line
= userInputStream.readLine();
if(line
== null) finished = true;
else
outStream.println(line);
}
while(!finished);
}
catch(IOException ex) {
error("Error
reading user input");
}
}
public
void shutdown(){
try{
connection.close();
}catch
(IOException ex) { error("IO error closing socket"); }
}
public
void error(String s){
System.out.println(s);
System.exit(1);
}
}
Note |
The TelnetApp class uses
the NVTPrinter,
NVTInputStream,
and NVTOutputStream
classes that are supplied in the following sections. You must type in the NVTPrinter.java, NVTInputStream.java,
and NVTOutputStream.java
files before compiling TelnetApp.java.
The Java compiler will automatically compile these files when TelnetApp.java is
compiled. |
You use the TelnetApp program in the same way as any other telnet program. But bear in mind that it is only a minimal telnet client. Run the program by invoking it with the hostname of a computer that supports telnet and the well-known telnet port number, port 23.
In the following example, I use the program to log into my account at CTS. Note that the program operates in half-duplex mode, so characters are echoed locally. I substituted asterisks (*) for my password. Take caution when using this program because it will display your password characters in the same manner as any other text that you type.
Also, notice that commands that I type are echoed by my cts.com host:
C:\java\jdg\ch26>java TelnetApp cts.com 23
Connected
to cts.com at port 23.
UNIX
System V Release 3.2 (crash.cts.com) (ttyp2)
login:
jaworski
Password:****
Last
successful login for jaworski: Tue Apr 09 23:17:46 PDT 1996 on ttyp34
Last
unsuccessful login for jaworski: Fri Apr 05 09:56:34 PST 1996 on ttyp9
Welcome
to CTSNET!
Enter
'help' for assistance and information.
1%
l
l
total
16
drwx------
2 jaworski guest 272 Sep 08 1995 Mail
drwxr-xr-x
2 jaworski guest 208 Dec 07 15:09 News
drwxr-xr-x
2 jaworski guest 224 Sep 08 1995 bin
drwxr-xr-x
2 jaworski guest 384 Apr 04 08:43 download
lrwxrwxrwx
1 root root 15 Mar 15 02:56 dropbox -> /ftp/j/jaworski
drwx------
2 jaworski guest 160 Dec 08 10:35 ga
drwx------
2 jaworski guest 288 Apr 08 09:49 mail
drwxr-xr-x
3 jaworski guest 112 Dec 01 12:20 writing
2%
exit
exit
3%
logout
Connection
broken.
C:\java\jdg\ch26>
The TelnetApp program creates an object of class PortTalk to perform its processing. This class extends the Thread class in order to implement multithreading capabilities. Its constructor uses the parameters passed in the TelnetApp command-line invocation to set up the connection to the specified host and port.
The run() method creates an object of the NVTPrinter class to interface with the destination host and invokes the processUserInput() method to interface with the user. The processUserInput() method reads a line at a time from the user's console and sends it to the telnet server.
The NVTPrinter class performs most of the interesting processing because it interfaces with the server. It does this using the NVTInputStream class covered in the next section. NVTPrinter is also implemented as a subclass of Thread. Its source code is shown in Listing 26.2.
Listing 26.2. The source code for the NVTPrinter class.
package jdg.ch26;
import
java.io.*;
public
class NVTPrinter extends Thread {
NVTInputStream
inStream;
public
NVTPrinter(NVTInputStream in) {
super();
inStream
= in;
}
public
void run() {
boolean
finished = false;
try
{
do
{
int
i = inStream.read();
if(i
== -1) finished = true;
else{
System.out.print((char)
i);
System.out.flush();
yield();
}
}
while(!finished);
System.out.println("\nConnection
broken.");
System.exit(0);
}
catch (IOException ex) {
System.out.println("NVTPrinter
error");
System.exit(1);
}
}
}
The NVTInputStream class implements the network virtual terminal input interface. Its source code is shown in Listing 26.3.
Listing 26.3. The source code for the NVTInputStream class.
package jdg.ch26;
import
java.io.*;
public
class NVTInputStream extends FilterInputStream {
byte
IAC = (byte) 0xff;
byte
DO = (byte) 0xfd;
byte
WILL = (byte) 0xfb;
byte
CR = 13;
byte
LF = 10;
int
WONT = 252;
int
DONT = 254;
int
BUFFER_SIZE = 1024;
OutputStream
out;
byte
lineBuffer[] = new byte[BUFFER_SIZE];
int
numBytes = 0;
public
NVTInputStream(InputStream inStream,OutputStream outStream) {
super(inStream);
out
= outStream;
}
public
int read() throws IOException {
boolean
recIAC;
int
i;
do
{
recIAC
= false;
i
= in.read();
if(i
== -1) return i;
byte
b = (byte) i;
if(b
== IAC) {
recIAC
= true;
int
cmd = in.read();
if(cmd
== -1) return cmd;
byte
b2 = (byte) cmd;
if(b2
== IAC) return 255;
else
if(b2 == DO) {
int
opt = in.read();
if(opt
== -1) return opt;
out.write(255);
out.write(WONT);
out.write(opt);
out.flush();
}else
if(b2 == WILL) {
int
opt = in.read();
if(opt
== -1) return opt;
out.write(255);
out.write(DONT);
out.write(opt);
out.flush();
}
}
}
while(recIAC);
return
i;
}
public
String readLine() throws IOException {
numBytes
= 0;
boolean
finished = false;
do
{
int
i = read();
if(i
== -1) return null;
byte
b = (byte) i;
if(b
== LF) {
if(numBytes>0)
{
if(lineBuffer[numBytes-1]
== 13)
return
new String(lineBuffer,0,0,numBytes-1);
}
}
lineBuffer[numBytes]
= b;
++numBytes;
}
while (!finished);
return
null;
}
}
NVTInputStream uses the network virtual terminal conventions, covered earlier in this chapter, to filter the input stream associated with the connection. It implements the basic read() method and also a convenient readLine() method.
The NVTOutputStream class provides an output analog to the NVTInputStream class. It implements the basic write() method according to the NVT conventions. It also provides a println() method that uses the carriage-return-line-feed (CR-LF) end-of-line conventions. Its source code is shown in Listing 26.4.
Listing 26.4. The source code for the NVTOutputStream class.
package jdg.ch26;
import
java.io.*;
public
class NVTOutputStream extends PrintStream {
int
IAC = 255;
byte
CR = 13;
byte
LF = 10;
public
NVTOutputStream(OutputStream outStream) {
super(outStream);
}
public
void write(int i) {
if(i
== IAC) super.write(i);
super.write(i);
}
public
void println(String s) {
super.print(s);
super.write(CR);
super.write(LF);
super.flush();
}
}
Although mail is sent on the Internet using a variety of protocols, the Simple Message Transfer Protocol (SMTP), described in Request for Comments (RFC) 821, is the basic protocol used to move mail from one host to another. SMTP consists of a mail sender, a mail receiver, and a set of line-oriented commands used to send mail from the sender to the receiver.
Note |
Requests for Comments are
numbered notes of the Internet community that are usually, but not always,
used to describe some aspect of the protocols or services used on the
Internet. |
RFC 821 describes the complete set of commands used by mail senders and receivers. Here I am using only a minimal subset of these commands to illustrate the development of an SMTP client, the mail sender.
An SMTP client connects to an SMTP server by establishing a connection to port 25 of the server's host. The server accepts the connection, sends a one-line ready notification to the client, and awaits client commands.
The client sends the HELO command with its hostname to introduce itself to the server. The server responds by sending a code that indicates that it is OK to initiate a mail transmission.
The client sends the MAIL command to the server to indicate that it has mail from a specific user. The server responds with a notification to proceed.
The client sends the RCPT command to identify the recipient of the e-mail. The server responds by telling the client whether or not the recipient is valid.
The client sends the DATA command to indicate that it is ready to send the message. The server responds by telling the client that it is OK to begin sending message data.
The client sends the message, a line at a time, terminating the message with a line containing a single period (.). A line of message text beginning with a period is sent by prepending an extra initial period to the message line.
The server acknowledges receiving the last line of text by sending an OK command to the client.
The client then terminates the connection by sending a QUIT command to the server. The server then responds by notifying the client that it is closing the connection.
The MailClientApp program illustrates the basic operation of a mail client program. It implements the basic SMTP commands described in the previous section. Its source code is shown in Listing 26.5.
Listing 26.5. The source code for the MailClientApp program.
import java.lang.*;
import
java.net.*;
import
java.io.*;
import
java.util.Vector;
import
jdg.ch26.NVTInputStream;
import
jdg.ch26.NVTOutputStream;
import
jdg.ch26.NVTPrinter;
public
class MailClientApp {
public
static void main(String args[]){
MessageInterface
messageInterface = new MessageInterface();
Message
msg = messageInterface.getMsg();
MailTalk
mailTalk = new MailTalk(msg);
mailTalk.run();
}
}
class
Message {
String
source;
String
destination;
String
subject;
String
text[];
public
Message() {
super();
}
public
void setDestination(String dest) {
destination
= dest;
}
public
String getDestination() {
return
destination;
}
public
void setSource(String src) {
source
= src;
}
public
String getSource() {
return
source;
}
public
String getDestinationHost() {
return
destination.substring(destination.indexOf('@')+1);
}
public
void setSubject(String subj) {
subject
= subj;
}
public
String getSubject() {
return
subject;
}
public
void setText(Vector txt) {
int
n = txt.size();
text
= new String[n];
for(int
i = 0; i< n; ++i) {
text[i]
= (String) txt.elementAt(i);
}
}
public
String[] getText() {
return
text;
}
}
class
MessageInterface {
Message
msg;
public
MessageInterface() {
msg
= new Message();
}
public
Message getMsg() {
try
{
System.out.print("From:
");
System.out.flush();
DataInputStream
inStream = new DataInputStream(System.in);
msg.setSource(inStream.readLine());
System.out.print("To:
");
System.out.flush();
msg.setDestination(inStream.readLine());
System.out.print("Subject:
");
System.out.flush();
msg.setSubject(inStream.readLine());
System.out.println("Enter
message text.");
System.out.println("Terminate
message text with an initial period.");
Vector
text = new Vector();
boolean
finished = false;
do
{
String
line = inStream.readLine();
if(endOfText(line))
finished = true;
else
text.addElement(line);
}
while(!finished);
msg.setText(text);
System.out.println("End
of message read.");
}catch
(IOException ex) {
System.out.println("IO
Exception");
System.exit(1);
}
return
msg;
}
boolean
endOfText(String s) {
if(s.length()
== 0) return false;
if(s.charAt(0)
== '.') return true;
return
false;
}
}
class
MailTalk {
//
Communication states
static
final int START = 0;
static
final int HELO = 1;
static
final int MAIL = 2;
static
final int RCPT = 3;
static
final int DATA = 4;
static
final int TEXT = 5;
static
final int QUIT = 6;
static
final int FINISHED = 9;
Socket
connection;
String
localHostName;
NVTOutputStream
outStream;
NVTInputStream
inStream;
Message
msg;
public
MailTalk(Message msg){
this.msg
= msg;
String
destination = msg.getDestinationHost();
int
port = 25;
try{
connection
= new Socket(destination,port);
localHostName
= InetAddress.getLocalHost().getHostName();
}catch
(UnknownHostException ex) { error("Unknown host"); }
catch
(IOException ex) { error("IO error creating socket"); }
try{
outStream
= new NVTOutputStream(connection.getOutputStream());
inStream
= new NVTInputStream(connection.getInputStream(),outStream);
}catch
(IOException ex) { error("IO error getting streams"); }
System.out.println("Connected
to "+destination+" at port "+port+".");
}
public
void run() {
sendMail();
shutdown();
}
public
void sendMail() {
try
{
int
state = START;
String
line;
do
{
line
= inStream.readLine();
if(line
== null) state = FINISHED;
else{
System.out.println(line);
switch(state)
{
case
START:
if(gotResponse(220,line)){
outStream.println("HELO
"+localHostName);
System.out.println(">>>HELO
"+localHostName);
state
= HELO;
}else
state=FINISHED;
break;
case
HELO:
if(gotResponse(250,line)){
outStream.println("MAIL
FROM:<"+msg.getSource()+">");
System.out.println(">>>MAIL
FROM:<"+msg.getSource()+">");
state
= MAIL;
}else
state=FINISHED;
break;
case
MAIL:
if(gotResponse(250,line)){
outStream.println("RCPT
TO:<"+msg.getDestination()+">");
System.out.println(">>>RCPT
TO:<"+msg.getDestination()+">");
state
= RCPT;
}else
state=FINISHED;
break;
case
RCPT:
if(gotResponse(250,line)){
outStream.println("DATA");
System.out.println(">>>DATA");
state
= DATA;
}else
state=FINISHED;
break;
case
DATA:
if(gotResponse(354,line)){
String
text[] = msg.getText();
int
len = text.length;
outStream.println("Subject:
"+msg.getSubject());
outStream.println("");
System.out.println("Subject:
"+msg.getSubject());
System.out.println("");
for(int
i=0;i<len;++i) {
if(text[i].length()
> 0 && text[i].charAt(0) == '.') {
outStream.println("."+text[i]);
System.out.println("."+text[i]);
}else{
outStream.println(text[i]);
System.out.println(">>>"+text[i]);
}
}
outStream.println(".");
System.out.println(">>>.");
state
= TEXT;
}else
state=FINISHED;
break;
case
TEXT:
if(gotResponse(250,line)){
outStream.println("QUIT");
System.out.println(">>>QUIT");
state
= QUIT;
}else
state=FINISHED;
break;
case
QUIT:
state=FINISHED;
break;
}
}
}
while(state != FINISHED);
}
catch(IOException ex) {
error("IO
Exception while sending mail");
}
}
boolean
gotResponse(int n,String s) {
try
{
int
responseCode = Integer.valueOf(s.trim().substring(0,3)).intValue();
String
line = s;
while(line.charAt(3)
== '-') {
line
= inStream.readLine();
System.out.println(line);
}
if(responseCode
== n) return true;
}catch(NumberFormatException
ex) {
}catch(IOException
ex){
}
return
false;
}
public
void shutdown(){
try{
connection.close();
}catch
(IOException ex) { error("IO error closing socket"); }
}
public
void error(String s){
System.out.println(s);
System.exit(1);
}
}
The MailClientApp program prompts you for the from: name that you want associated with the sent message. SMTP is inherently insecure and will allow you to send e-mail using the e-mail address of another person as the from: address. In the example, I send a message using my daughter's e-mail address to myself. The subject of the message is Test Message and it contains a mere two lines of text. The following output shows a sample dialog with the MailClientApp program:
C:\java\jdg\ch26>java MailClientApp
From:
emily@jaworski.com
To:
jamie@jaworski.com
Subject:
Test Message
Enter
message text.
Terminate
message text with an initial period.
This
is a test.
It
is only a test.
.
End
of message read.
Connected
to jaworski.com at port 25.
220-jaworski.com
Sendmail 8.6.12/8.6.9 ready at Wed, 10 Apr 1996 00:33:31 -0700
220
ESMTP spoken here
>>>HELO
athome.jaworski.com
250
jaworski.com Hello athome.jaworski.com [204.212.153.194], pleased to meet you
>>>MAIL
FROM:<emily@jaworski.com>
250
<emily@jaworski.com>... Sender ok
>>>RCPT
TO:<jamie@jaworski.com>
250
<jamie@jaworski.com>... Recipient ok
>>>DATA
354
Enter mail, end with "." on a line by itself
Subject:
Test Message
>>>This
is a test.
>>>It
is only a test.
>>>.
250
AAA02243 Message accepted for delivery
>>>QUIT
221
jaworski.com closing connection
C:\java\jdg\ch26>
After the message is received by the e-mail client, it connects to my SMTP server and sends the message using the SMTP commands summarized earlier in this chapter.
The >>> arrows indicate commands that were sent by the program.
Web browsers are the most popular client programs found on the Internet. They allow users to download and display Web pages, usually one at a time. The program shown in Listing 26.6 allows the user to specify a list of Web pages to be retrieved, and retrieves these Web pages and stores them on the local file system. This is an example of how custom Web clients can be implemented in Java.
Listing 26.6. The source code for the WebFetchApp program.
import java.util.Vector;
import
java.io.*;
import
java.net.*;
public
class WebFetchApp {
public
static void main(String args[]){
WebFetch
fetch = new WebFetch();
fetch.run();
}
}
class
WebFetch {
String
urlList = "url-list.txt";
Vector
URLs = new Vector();
Vector
fileNames = new Vector();
public
WebFetch() {
super();
}
public
void getURLList() {
try
{
DataInputStream
inStream = new DataInputStream(new FileInputStream(urlList));
String
inLine;
while((inLine
= inStream.readLine()) != null) {
inLine
= inLine.trim();
if(!inLine.equals(""))
{
int
tabPos = inLine.lastIndexOf('\t');
String
url = inLine.substring(0,tabPos).trim();
String
fileName = inLine.substring(tabPos+1).trim();
URLs.addElement(url);
fileNames.addElement(fileName);
}
}
}catch(IOException
ex){
error("Error
reading "+urlList);
}
}
public
void run() {
getURLList();
int
numURLs = URLs.size();
for(int
i=0;i<numURLs;++i)
fetchURL((String)
URLs.elementAt(i),(String) fileNames.elementAt(i));
System.out.println("Done.");
}
public
void fetchURL(String urlName,String fileName) {
try{
URL
url = new URL(urlName);
System.out.println("Getting
"+urlName+"...");
File
outFile = new File(fileName);
PrintStream
outStream = new PrintStream(new FileOutputStream(outFile));
DataInputStream
inStream = new DataInputStream(url.openStream());
String
line;
while
((line = inStream.readLine())!= null) outStream.println(line);
inStream.close();
outStream.close();
}catch
(MalformedURLException ex){
System.out.println("Bad
URL");
}catch
(IOException ex){
System.out.println("IOException
occurred.");
}
}
public
void error(String s){
System.out.println(s);
System.exit(1);
}
}
To use the program, create a file named url-list.txt that contains the names of the URLs you want to retrieve and the names of the files in which you want them stored. The following url-list.txt file was used to retrieve some pretty famous Web pages; it is included on the CD, in the \jdg\ch26 directory:
http://www.yahoo.com
yahoo.htm
http://www.cnn.com
cnn.htm
http://home.netscape.com
netscape.htm
The output generated for the WebFetchApp program is as follows:
C:\java\jdg\ch26>java WebFetchApp
Getting
http://www.yahoo.com...
Getting
http://www.cnn.com...
Getting
http://home.netscape.com...
Done.
C:\java\jdg\ch26>
In this chapter you have learned how to write client programs that implement the client end of Internet client/server applications. You have learned about the common client programs found on the Internet and how they are structured. You have developed a simple telnet client, an e-mail program, and the Web fetcher program. In Chapter 27, "Server Programs," you'll learn how to write simple server applications.