Exercise: the client/server model

a mix of clients and servers to play off against one another

 

The client-sever model

The client-server model joins programs in pairs so that they may communicate. The idea is that sometimes one program would benefit if another would give it something, or do some work for it. The model provides a mechanism for asking.

The worker program is called the "server," the beneficiary program the "client," and the request mechanism the "socket programming interface." The model says nothing about what the server should give the client or do for it. It only creates the framework enabling delivery-- of the request from client to server, and of fulfillment (whatever was requested) from server to client.

The distinction between different kinds of clients lies in what they trade with each other. The client might ask the server for something the server already has (a copy of an existing file). Or, something the server does not have but needs to produce on the fly (a copy of current traffic conditions). Or, a processed version of something the client first sends to the server (a current balance corresponding to a supplied account number). Example servers: file servers give out wholesale copies of entire disk files, mail servers supply short messages, web servers provide to-be-displayed information. But there is no distinction among them in their programming framework. This socket programming interface is a programming architecture that is their common denominator, and gives them an identifiable, signature structural organization you can see if you glance at their source code.

This exercise provides several demonstration clients and servers in source code form. By examining, compiling, modifying, and running them their organization and methods should become unmistakably clear. Each sample server's code contains comments to prominently demarcate the processing section where the server does its work. This comes, sensibly, after receiving what the client sends and before sending a reply back. These programs are minimalistic, for tutorial clarity, but the architecture they display is shared by the "big boys" like the Outlook Express mail client or the Apache web server for example. The difference is that the big boys have major, beefed-up processing sections. Once they get something from the client, they do a much more elaborate job with it than our sample programs do. But the point is they are otherwise constructed the same way.

Clients and servers for this exercise

In this exercise we use a variety of clients and servers, 5 of each. We use certain clients against certain servers. It doesn't make sense to use just any client against a particular server. A server has expectations as to what it will receive from the client (these rules it plays by are called its "protocol"). If you use a client that sends something else, the server won't understand and won't respond meaningfully (so please don't use an email client to browse a web page, nor an ftp client to check your mail). In our case, here are the 5 clients and 5 servers.

Client What does it send? How to specify server's IP & port Default server IP, port, (file)
client3.c a single character edit source code & recompile 127.0.0.1 & 10001
uppoerecho_client.c a character string edit source code & recompile 127.0.0.1 & 10002
web_client.c an http "GET" request for a file edit source code & recompile 127.0.0.1 & 10003 (test.txt)
lynx an http "GET" request for a file command line:
lynx 127.0.0.1:10003/test.txt
n/a
firefox an http "GET" request for a file URL bar:
http://127.0.0.1:10003/test.txt
n/a

 

Server What does it do with what it gets, and send back? How to specify its port Its default port
server3.c increments the character ("A" becomes "B") edit source code & recompile 10001
upperecho_server.c makes the string all uppercase edit source code & recompile 10002
web_server.c send the file named in the request edit source code & recompile 10003
webserv.c depends on requested file
 txt - send file contents
 cgi - run and send output
 directory - send dir listing
command line:
webserv 10003
n/a
apache depends on requested file
and configuration factors
edit configuration file:
/etc/httpd/conf/httpd.conf
80

 

In the GUI, open two terminal windows, a server window to run servers and a client window in which to run clients off against them. Operate as root. Let's look at each of our servers in turn, playing against them whichever client(s) are appropriate. Arrange them to be non-overlapping so you can see what's happening in both simultaneously.

Install sample programs

First obtain the tar archive that contains the files for this exercise. It's called clientserver_samples.tar.gz. Select your server window. There, in your home directory create a directory named clientserver:

cd
mkdir clientserver

Put clientserver_samples.tar.gz there. Then unpack its contents:

cd
cd clientserver
tar -xzvf clientserver_samples.tar.gz

Examine what you have:

ls -l

First server: server3.c

This is perhaps the simplest conceivable server. It is written to receive a single character from a client that contacts it. It increments that character's value then returns the result. So it returns B to a client that sends A, C to a client that sends B, and so on. We'll automate compilation for the other servers, but let's compile this one manually:

gcc client3.c -o client3
gcc server3.c -o server3

You're in your server window. Run the server:

./server3

This server, as written, is listening on port 10001 of the local machine. Now select (click on) your client window. As written, the client tries to communicate with port 10001 of the local machine, to which it sends an "A" character. Run the client and watch the activity in both windows:

cd
cd clientserver
./client3

Study the source code for both and understand what happened. Find the line in the client's source where it sent an A. Find the lines in the server where it turned it into a B and where it shipped the B back. To kill the server, select the server window and issue a ctrl-C in it.

Second server: upperecho_server.c

Here's a second example of the characteristic volley-and-return behavior of clients and servers, with a different pair of them. This particular server accepts a character string, turns it all to uppercase, and that's what it returns. You send it "Hello" and it sends you "HELLO" for example. This server, as written, is listening on port 10002 of the local machine. As written, its client tries to communicate with port 10002 of the local machine, to which it sends a character string. Don't compile this one manually. Instead you can compile all the needed programs using the automated features of the make utility. In the server window, just run:

make

Several things get compiled, including upperecho_server.c and its counterpart upperecho_client.c. Select the server window and run:

./upperecho_server

Select the client window and run

./upperecho_client

Run the client again, supplying the string to be sent on the command line:

./upperecho_client "I see England, I see France."

Although client3 was not intended to operate as a client to upperecho_server, nor upperecho_client intended for server3, you could run them that way. Do it. Now, while the currently running server is upperecho_server, in your current client window edit client3 to change the uppercase A it sends to a lowercase z, and to change its target port from 10001 to the port being used by the current server, namely 10002. (If you know how to use a character mode editor like vi, use it. If not, look for a graphical editor such as gedit. It is comparable to notepad and can be used more intuitively.) After editing client3.c, recompile and rerun it:

gcc client3.c -o client3
./client3

Make sure you understand the activity you see in the two windows. Now go to the server window and terminate upperecho_server with a ctrl-C keystroke. Then in its place run server3:

./server3

and, switching to the client window, run the upperecho client. First, however, recall that server3 is coded to listen on port 10001. You must "tune" upperecho_client.c to talk to that same port. Make the requisite editing change, then to compile and run:

make
./upperecho_client "unix wants everything to be in lowercase"

Study the activity in the windows. Make sure you can explain what you see. If you can't, ask.

Third server: web_server.c

These examples so far are interesting but not very practical. We don't really need automated services for shifting letters to their alphabetic successors and capitalizing text for us. More practical are web servers. Their main job to return requested content. The simplest is if that content pre-exists in a file, waiting to be delivered on demand at a moment's notice. Then the client just specifies the desired file to the server in a request. There's a particular, specialized way to formulate such a request, called http. The client is supposed to decide what file it wants then send its name to the server preceded by "GET" and followed by "HTTP/1.0". So a client wanting the server's file "foo" must send "GET /foo HTTP/1.0" to the server, using the same mechanisms and coding (socket API) by which the previous servers sent their various characters and strings.

In the server window terminate the currently running server (ctrl-C). Run this one:

./web_server

This server, as written, is listening on port 10003 of the local machine. As written, the client tries to communicate with port 10003 of the local machine, to which it sends "GET /test.txt HTTP/1.0". So the file it wants is test.txt. Switch to the client window and run the client:

./web_client

Study the resulting activity in the two windows. There are 3 other files the client can ask for: test1.cgi, test2.cgi, and test3.cgi. In the client window, successively edit, recompile, and rerun web_client asking for each of these in turn. Does the server treat the requests for these any differently than it did the request for test.txt (other than substituting the actually-requested file)?

There are 2 other clients that send pretty much identical http file requests as does web_server (i.e., "GET /foo HTTP/1.0"). One of them is a character mode program called lynx, the other a graphical mode program called firefox. Make lynx ask for the same files as did web_client. Although that file's name is hard-coded in web_client so that you must re-edit, re-compile, re-run whenever you want a different file, lynx lets you name the file on its command line. So in the client window, once again go get those same 4 files:

lynx  http://127.0.0.1:10003/test.txt
lynx  http://127.0.0.1:10003/test1.cgi
lynx  http://127.0.0.1:10003/test2.cgi
lynx  http://127.0.0.1:10003/test3.cgi

Once in lynx, you can get out by pressing q then y.

Let's get those 4 files a 3rd time, with firefox this time. With firefox, the way to specify the file you want to get is not to edit its code as you did with web_client.c (you don't have its code), nor to supply the filename on the command line as you did with lynx (firefox doesn't have a command line). Rather, firefox has a graphical text window near the top. You specify your target file in there, then press Enter. First, run firefox from within the client window (alternatively you could run it from a graphical menu system, if offered):

firefox &

In the text window of the resulting graphical application, successively type the following (then Enter) to retrieve the 4 files:

  http://127.0.0.1:10003/test.txt
  http://127.0.0.1:10003/test1.cgi
  http://127.0.0.1:10003/test2.cgi
  http://127.0.0.1:10003/test3.cgi

Fourth server: webserv.c

Let's switch servers. In the server window, shut down web_server (issue ctrl-C). Then, start a different web server, webserv:

./webserv

It won't run unless you give it a port number to use, on its command line. It's arbitrary (as long as not conflicting with a port number already in use by some other program) so let's pick 1239:

./webserv 1239

Then in the client window:

 lynx http://127.0.0.1:1239/test.txt
 lynx http://127.0.0.1:1239/test1.cgi
 lynx http://127.0.0.1:1239/test2.cgi
  lynx http://127.0.0.1:1239/test3.cgi

And in firefox:

http://127.0.0.1:1239/test.txt
http://127.0.0.1:1239/test1.cgi
http://127.0.0.1:1239/test2.cgi
http://127.0.0.1:1239/test3.cgi

Note that the results seen are not exactly the same with webserv as they were with web_server. That's because they are not returning the same thing in all cases. web_server just returns the file you name. webserv does not. Rather, if the file has .cgi extension, webserve tries to execute the file you name. Assuming the file can and does run, what webserv returns is the output printed by that file. How do I know? Please look at the source code for the two, and tell me.

Now ask webserv for a directory, instead of a file. Have both lynx and firefox do the asking. In the client window:

 lynx http://127.0.0.1:1239/test.dir/

and in firefox:

http://127.0.0.1:1239/test.dir

You see a listing of that directory's contents. In webserve.c, locate the portion of code responsible for that.