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 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 a 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 interoperate with the application. When developing a proprietary application, the developer must be careful not to use one of the the well-known port numbers defined in the RFCs.
In this and the next section, we will examine the key issues for the development of 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. TCP is connection-oriented and provides a reliable byte stream channel through which data flows between two endsystems. 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, including [Stevens 1990] , [Frost 1994] and [Kurose 1996] .
Now let's to 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 can not 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 (i.e., 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 (i.e., 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.6.2.
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 flowing 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 characters 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 characters flow into the Internet.
Once the 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.
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.6-3.
Figure 2.6-3: 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, i.e., 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 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.
class
TCPClient {
public static void main(String argv[]) throws Exception
{......}
}
The above 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, 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 the user's standard output.
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 variable "host name" must be replaced with the host name of the server (e.g., "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 applications 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 diagram above.)
sentence = inFromUser.readLine();
The above line places a line typed by 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.
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).
import java.io.*;
import java.net.*;
class TCPServer {
public
static void main(String argv[]) throws Exception
{
String clientSentence;
String capitalizedSentence;
ServerSocket welcomeSocket = new ServerSocket(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 us now take a look at the lines in TCPServer.java. We will not comment on the lines which 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);
The above 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 following line is:
Socket connectionSocket = welcomeSocket.accept();
The above 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 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.
That completes our analysis of the TCP program pair. Recall that TCP provides a reliable data transfer service. This implies, in particular, that if one the user's characters gets corrupted in the network, then the client host will retransmit the character, thereby providing correct delivery of the data. These retransmissions are completely transparent to the application programs. The DNS lookup is also transparent to the application programs.
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 which 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.
[RFC 959] J.B. Postel and J.K. Reynolds, "Filel
Transfer Protocol," [RFC
959], October 1985.
[Stevens 1990] W.R. Stevens, Unix Network
Porgramming, Prentice-Hall, Englewood Cliffs, N.J.
[Frost 1994] J. Frost, BSD Sockets:
A Quick and Dirty Primer, http://world.std.com/~jimf/papers/sockets/sockets.html
[Kurose 1996] J.F. Kurose, Unix Network
Programming, http://www-aml.cs.umass.edu/~amldemo/courseware/intro.html