| This and the
subsequent sections provide an introduction to network application development.
Recall from Section 2.1 that the core of a network application consists
of a pair of programs--a client program and a server program. When these
two programs are executed, a client and server process are created, and
these two processes communicate with each other by reading from and writing
to sockets. When creating a network application, the developer's main task
is to write the code for both the client and server programs.
There are two
sorts of client/server applications. One sort is a client/server application
that is an implementation of a protocol standard defined in an RFC.
For such an implementation, the client and server programs must conform
to the rules dictated by the RFC. For example, the client program could
be an implementation of the FTP client, defined in RFC 959, and the server
program could be an implementation of the FTP server, also defined in RFC
959. If one developer writes code for the client program and an independent
developer writes code for the server program, and both developers carefully
follow the rules of the RFC, then the two programs will be able to interoperate.
Indeed, most of today's network applications involve communication between
client and server programs that have been created by independent developers.
(For example, a Netscape browser communicating with an Apache Web server,
or an FTP client on a PC uploading a file to a Unix FTP server.) When a
client or server program implements a protocol defined in an RFC, it should
use the port number associated with the protocol. (Port numbers were briefly
discussed in Section 2.1. They will be covered in more detail in the next
chapter.)
The other sort
of client/server application is a proprietary client/server application.
In this case the client and server programs do not necessarily conform
to any existing RFC. A single developer (or development team) creates both
the client and server programs, and the developer has complete control
over what goes in the code. But because the code does not implement a public-domain
protocol, other independent developers will not be able to develop code
that interoperates with the application. When developing a proprietary
application, the developer must be careful not to use one of the well-known
port numbers defined in the RFCs.
In this and
the next section, we will examine the key issues in developing a proprietary
client/server application. During the development phase, one of the first
decisions the developer must make is whether the application is to run
over TCP or over UDP. Recall that TCP is connection-oriented and provides
a reliable byte- stream channel through which data flows between
two end systems. UDP is connectionless and sends independent packets
of data from one end system to the other, without any guarantees about
delivery.
In this section
we develop a simple client application that runs over TCP; in the subsequent
section, we develop a simple client application that runs over UDP. We
present these simple TCP and UDP applications in Java. We could have written
the code in C or C++, but we opted for Java for several reasons. First,
the applications are more neatly and cleanly written in Java; with Java
there are fewer lines of code, and each line can be explained to the novice
programmer without much difficulty. Second, client/server programming in
Java is becoming increasingly popular, and may even become the norm in
upcoming years. Java is platform-independent, it has exception mechanisms
for robust handling of common problems that occur during I/O and networking
operations, and its threading facilities provide a way to easily implement
powerful servers. But there is no need to be frightened if you are not
familiar with Java. You should be able to follow the code if you have experience
programming in another language.
For readers
who are interested in client/server programming in C, there are several
good references available [Stevens
1997; Frost
1994; Kurose
1996].
2.6.1: Socket Programming
with TCP
Recall from Section
2.1 that processes running on different machines communicate with each
other by sending messages into sockets. We said that each process was analogous
to a house and the process's socket is analogous to a door. As shown in
Figure 2.23, the socket is the door between the application process and
TCP. The application developer has control of everything on the application-layer
side of the socket; however, it has little control of the transport-layer
side. (At the very most, the application developer has the ability to fix
a few TCP parameters, such as maximum buffer size and maximum segment sizes.)
Figure 2.23:
Process communicating through TCP sockets
Now let's take
a little closer look at the interaction of the client and server programs.
The client has the job of initiating contact with the server. In order
for the server to be able to react to the client's initial contact, the
server has to be ready. This implies two things. First, the server program
cannot be dormant; it must be running as a process before the client attempts
to initiate contact. Second, the server program must have some sort of
door (that is, socket) that welcomes some initial contact from a client
running on an arbitrary machine. Using our house/door analogy for a process/socket,
we will sometimes refer to the client's initial contact as "knocking on
the door."
With the server
process running, the client process can initiate a TCP connection to the
server. This is done in the client program by creating a socket object.
When the client creates its socket object, it specifies the address of
the server process, namely, the IP address of the server and the port number
of the process. Upon creation of the socket object, TCP in the client initiates
a three-way handshake and establishes a TCP connection with the server.
The three-way handshake is completely transparent to the client and server
programs.
During the three-way
handshake, the client process knocks on the welcoming door of the server
process. When the server "hears" the knocking, it creates a new door (that
is, a new socket) that is dedicated to that particular client. In our example
below, the welcoming door is a ServerSocket object that we call
the welcomeSocket. When a client knocks on this door, the program
invokes welcomeSocket's accept() method, which creates
a new door for the client. At the end of the handshaking phase, a TCP connection
exists between the client's socket and the server's new socket. Henceforth,
we refer to the new socket as the server's connection socket.
From the application's
perspective, the TCP connection is a direct virtual pipe between the client's
socket and the server's connection socket. The client process can send
arbitrary bytes into its socket; TCP guarantees that the server process
will receive (through the connection socket) each byte in the order sent.
Furthermore, just as people can go in and out the same door, the client
process can also receive bytes from its socket and the server process can
also send bytes into its connection socket. This is illustrated in Figure
2.24.
Figure 2.24:
Client socket, welcoming socket, and connection socket
Because sockets
play a central role in client/server applications, client/server application
development is also referred to as socket programming. Before providing
our example client/server application, it is useful to discuss the notion
of a stream. A stream is a sequence of characters that flow into or out
of a process. Each stream is either an input stream for the process or
an output stream for the process. If the stream is an input stream, then
it is attached to some input source for the process, such as standard input
(the keyboard) or a socket into which data flow from the Internet. If the
stream is an output stream, then it is attached to some output source for
the process, such as standard output (the monitor) or a socket out of which
data flow into the Internet.
2.6.2: An Example
Client/Server Application in Java
We will use the
following simple client/server application to demonstrate socket programming
for both TCP and UDP:
A client reads
a line from its standard input (keyboard) and sends the line out its socket
to the server.
The server reads
a line from its connection socket.
The server converts
the line to uppercase.
The server sends
the modified line out its connection socket to the client.
The client reads
the modified line from its socket and prints the line on its standard output
(monitor).
Let us begin
with the case of a client and server communicating over a connection- oriented
(TCP) transport service. Figure 2.25 illustrates the main socket-related
activity of the client and server.
Figure 2.25:
The client/server application, using connection-oriented transport services
Next we provide
the client/server program pair for a TCP implementation of the application.
We provide a detailed, line-by-line analysis after each program. The client
program is called TCPClient.java, and the server program is called
TCPServer.java. In order to emphasize the key issues, we intentionally
provide code that is to the point but not bullet proof. "Good code" would
certainly have a few more auxiliary lines.
Once the two
programs are compiled on their respective hosts, the server program is
first executed at the server, which creates a process at the server. As
discussed above, the server process waits to be contacted by a client process.
When the client program is executed, a process is created at the client,
and this process contacts the server and establishes a TCP connection with
it. The user at the client may then "use" the application to send a line
and then receive a capitalized version of the line.
TCPClient.java
Here is the
code for the client side of the application:
import java.io.*;
import java.net.*;
class TCPClient {
public static void main(String argv[]) throws Exception
{
String sentence;
String modifiedSentence;
BufferedReader inFromUser =
new BufferedReader(
new InputStreamReader(System.in));
Socket clientSocket = new Socket("hostname", 6789);
DataOutputStream outToServer =
new DataOutputStream(
clientSocket.getOutputStream());
BufferedReader inFromServer =
new BufferedReader(new InputStreamReader(
clientSocket.getInputStream()));
sentence = inFromUser.readLine();
outToServer.writeBytes(sentence + '\n');
modifiedSentence = inFromServer.readLine();
System.out.println("FROM SERVER: " +
modifiedSentence);
clientSocket.close();
}
}
The program TCPClient
creates three streams and one socket, as shown in Figure 2.26.
Figure 2.26:
TCPClient has three streams and one socket
The socket is
called clientSocket. The stream inFromUser is an input
stream to the program; it is attached to the standard input (that is, the
keyboard). When the user types characters on the keyboard, the characters
flow into the stream inFromUser. The stream inFromServer
is another input stream to the program; it is attached to the socket. Characters
that arrive from the network flow into the stream inFromServer.
Finally, the stream outToServer is an output stream from the program;
it is also attached to the socket. Characters that the client sends to
the network flow into the stream outToServer.
Let's now take
a look at the various lines in the code.
import java.io.*;
import java.net.*;
java.io
and java.net are java packages. The java.io package contains
classes for input and output streams. In particular, the java.io
package contains the BufferedReader and DataOutputStream
classes, classes that the program uses to create the three streams previously
illustrated. The java.net package provides classes for network
support. In particular, it contains the Socket and ServerSocket
classes. The clientSocket object of this program is derived from
the Socket class.
class TCPClient {
public static void main(String argv[]) throws Exception
{......}
}
So far, what we've
seen is standard stuff that you see at the beginning of most Java code.
The first line is the beginning of a class definition block. The keyword
class begins the class definition for the class named TCPClient.
A class contains variables and methods. The variables and methods of the
class are embraced by the curly brackets that begin and end the class definition
block. The class TCPClient has no class variables and exactly
one method, the main() method. Methods are similar to the functions
or procedures in languages such as C; the main method in the Java language
is similar to the main function in C and C++. When the Java interpreter
executes an application (by being invoked upon the application's controlling
class), it starts by calling the class's main method. The main method then
calls all the other methods required to run the application. For this introduction
into socket programming in Java, you may ignore the keywords public,
static, void, main, and throws Exceptions
(although you must include them in the code).
String sentence;
String modifiedSentence;
These above two
lines declare objects of type String. The object sentence
is the string typed by the user and sent to the server. The object modifiedSentence
is the string obtained from the server and sent to the user's standard
output.
The above line
creates the stream object inFromUser of type Buffered Reader.
The input stream is initialized with System.in, which attaches
the stream to the standard input. The command allows the client to read
text from its keyboard.
Socket clientSocket
= new Socket("hostname", 6789);
The above line
creates the object clientSocket of type Socket. It also
initiates the TCP connection between client and server. The string "hostname"
must be replaced with the host name of the server (for example, "fling.
seas.upenn.edu"). Before the TCP connection is actually initiated,
the client performs a DNS look-up on the hostname to obtain the host's
IP address. The number 6789 is the port number. You can use a different
port number; but you must make sure that you use the same port number at
the server side of the application. As discussed earlier, the host's IP
address along with the application's port number identifies the server
process.
DataOutputStream outToServer =
new DataOutputStream(clientSocket.getOutputStream());
BufferedReader inFromServer =
new BufferedReader(new inputStreamReader(
clientSocket.getInputStream()));
The above two lines
create stream objects that are attached to the socket. The outToServer
stream provides the process output to the socket. The inFromServer
stream provides the process input from the socket (see Figure 2.26).
sentence
= inFromUser.readLine();
The above line
places a line typed by the user into the string sentence. The
string sentence continues to gather characters until the user ends the
line by typing a carriage return. The line passes from standard input through
the stream inFromUser into the string sentence.
outToServer.writeBytes(sentence
+ '\n');
The above line
sends the string sentence augmented with a carriage return into
the outToServer stream. The augmented sentence flows through the
client's socket and into the TCP pipe. The client then waits to receive
characters from the server.
modifiedSentence
= inFromServer.readLine();
When characters
arrive from the server, they flow through the stream inFromServer
and get placed into the string modifiedSentence. Characters continue
to accumulate in modifiedSentence until the line ends with a carriage
return character.
System.out.println("FROM
SERVER " + modifiedSentence);
The above line
prints to the monitor the string modifiedSentence returned by
the server.
clientSocket.close();
This last line
closes the socket and, hence, closes the TCP connection between the client
and the server. It causes TCP in the client to send a TCP message to TCP
in the server (see Section 3.5).
TCPServer.java
Now let's take
a look at the server program.
import java.io.*;
import java.net.*;
class TCPServer {
public static void main(String argv[]) throws Exception
{
String clientSentence;
String capitalizedSentence;
ServerSocket welcomeSocket = new Server Socket
(6789);
while(true) {
Socket connectionSocket = welcomeSocket.
accept();
BufferedReader inFromClient =
new BufferedReader(new InputStreamReader(
connectionSocket.getInputStream()));
DataOutputStream outToClient =
new DataOutputStream(
connectionSocket.getOutputStream());
clientSentence = inFromClient.readLine();
capitalizedSentence =
clientSentence.toUpperCase() + '\n';
outToClient.writeBytes(capitalizedSentence);
}
}
}
TCPServer
has many similarities with TCPClient. Let's now take a look at
the lines in TCPServer.java. We will not comment on the lines
that are identical or similar to commands in TCPClient.java.
The first line
in TCPServer that is substantially different from what we saw
in TCPClient is:
ServerSocket
welcomeSocket = new ServerSocket(6789);
That line creates
the object welcomeSocket, which is of type ServerSocket.
The WelcomeSocket, as discussed above, is a sort of door that
waits for a knock from some client. The port number 6789 identifies the
process at the server. The next line is:
Socket connectionSocket
= welcomeSocket.accept();
This line creates
a new socket, called connectionSocket, when some client knocks
on welcomeSocket. TCP then establishes a direct virtual pipe between
clientSocket at the client and connectionSocket at the
server. The client and server can then send bytes to each other over the
pipe, and all bytes sent arrive at the other side in order. With connectionSocket
established, the server can continue to listen for other requests from
other clients for the application using welcomeSocket. (This version
of the program doesn't actually listen for more connection requests, but
it can be modified with threads to do so.) The program then creates several
stream objects, analogous to the stream objects created in clientSocket.
Now consider:
capitalizedSentence
= clientSentence.toUpperCase() + '\n';
This command
is the heart of the application. It takes the line sent by the client,
capitalizes it, and adds a carriage return. It uses the method toUpperCase().
All the other commands in the program are peripheral; they are used for
communication with the client.
To test the
program pair, you install and compile TCPClient.java in one host
and TCPServer.java in another host. Be sure to include the proper
host name of the server in TCPClient.java. You then execute TCPServer.class,
the compiled server program, in the server. This creates a process in the
server that idles until it is contacted by some client. Then you execute
TCPClient.class, the compiled client program, in the client. This
creates a process in the client and establishes a TCP connection between
the client and server processes. Finally, to use the application, you type
a sentence followed by a carriage return.
To develop your
own client/server application, you can begin by slightly modifying the
programs. For example, instead of converting all the letters to uppercase,
the server can count the number of times the letter "s" appears and return
this number. |