Description | Examples | Makefiles | Experiments | Hints | Hand In | Grading
The main purpose of this project is to give you some basic experience in writing a distributed system, albeit a simple one, while building upon some of your OS knowledge. You will write a basic remote shell in which a user specifies an arbitrary shell command to be executed, and that command is sent over a network and executed on a remote server.
You are to write a server and a client:
Server: The server runs on the remote machine. It binds
to a TCP/IP socket at a port known to the client. When it receives a
connection, it fork()
s a child process to handle the
connection. The parent process loops back to wait for more
connections. The child process first authenticates
the client via the protocol:
If authenticated, the server executes the given shell command via
an exec()
-flavored call, returning all data sent
to stdout
and stderr
to the client. The
server can assume the shell command does not use stdin
.
Upon completing the command, the server exits. Note, that the
original server process will still be around, waiting for additional
connections.
Client: The client runs on the local machine. From the
command line, the user specifies the host where the server resides and
the command to be executed. The client then connects to the server
via a socket and transmits the username to start the authentication
protocol shown in the server above. If authentication is successful,
the client sends the command. The client displays any output received
from the server to stdout
and then exits.
After you have implemented your Distributed Shell and debugged it carefully, you then design experiments to measure: 1) the amount of time required (the latency, in milliseconds) to setup a connection to the server, authenticate, and tear it down, and 2) the maximum throughput (in bits per second) from the server to the client. For both sets of measurements, you need to do multiple runs in order to account for any variance in the data between runs.
To measure the connection-tear down time, consider forcing the
client to make a call to the the server that does not have the server
to do an exec()
or any other significant processing.
Since the time scale for the first test is very small, you will
measure the time for many operations and then divide by the number of
operations performed. You need to build a harness (a program,
shell script, perl script or something similar) to make repeated
connection-tear down requests.
To measure throughput, consider forcing the server to send a large file (of a known size) to the client. Note, the client output, by default, goes to stdout so you may want to consider redirecting stdout to a file (via the ">" redirection shell operator).
In order to record the time on your computer (instead of, say,
looking at the clock on the wall) you can use the
gettimeofday()
system call from a program,
or time
from a shell (note, some shells have a
built-in time
command, too). You can also do something
similar in a scripting language of your choice
(e.g., localtime()
in perl).
When your experiments are complete, you must turn in a brief (1-2 page) write-up with the following sections:
To get help information about specific Unix commands, use the "man"
command. For instance, entering "man tcsh" will display the manual
page entry for the tcsh. If you specify a number after the word "man"
it looks in the indicated section number (e.g. "man 2 bind") to
display the bind()
system call instead of bash's
built-in bind
command.
The following system calls for setting up your sockets may be helpful:
connect()
accept()
socket()
listen()
bind()
close()
send()
recv()
getservbyname()
gethostname()
gethostbyname()
gethostbyaddr()
You might also see Beej's Guide to Network Programming for socket information.
The following system calls for the shell aspects of your programs might be helpful:
fork()
- to create a new process.
execve()
- to execute a file. The call
execvp()
may be particularly useful.
getopt()
- help in parsing command line arguments.
strtok()
- to help in parsing strings.
dup2()
- to redirect stdout, stderr to socket
The following system call will be useful for the authenticating part of your program:
crypt()
- create a cryptographic hash. Note, need
#define _XOPEN_SOURCE
before #include
<unistd.h>
.
Some sample code may be useful:
talk-tcp.c
and listen-tcp.c
- helpful
samples for doing socket code.
fork.c
-
showing the simple use of the fork()
call.
execl.c
-
showing simple use of the execl()
call.
get-opt.c
- code that parses command line arguments (fairly)
painlessly.
Beware of the living dead! When the child process of a server
exits, it cannot be reclaimed until the parent gathers its resource
statistics (typically via a wait()
call or a variant).
You might check out a waitpid()
since wait()
blocks, or use wait()
in conjunction with a
signal()
that triggers when a child exits.
When running your experiments, you need to be careful of processes in the background (say, a Web browser downloading a page or a compilation of a kernel) that may influence your results. While multiple data runs will help spot periods of extra system activity, try to keep your system "quiet" so the results are consistent (and reproducible).
You might look at the brief slides for some overview information.
For added security, a "real" server would likely use
chroot()
to change the root directory of the process.
For example, many anonymous ftp servers use chroot()
to
make the top directory level /home/ftp
or something
similar. Thus, a malicious user is less likely to compromise the
system since it doesn't have access to the root file system. Note,
however, chroot()
requires root privilege to run, making
it unavailable to the common user (e.g., on
the ccc.wpi.edu
machines). For developing on your own
system (say, a Linux box), I encourage you to explore
using chroot()
(do a "man 2 chroot
" for more
information). Please note if you do this in a README file or similar
documentation when you turn in your program. Make sure your programs
run on the CCC machines.
Here are some examples. The server:
claypool 94 ccc% ./server -h distributed shell server usage: server [flags], where flags are: -p # port to serve on (default is 6013) -d dir directory to serve out of (default is /home/claypool/dsh) -h this help message claypool 95 ccc% ./server ./server activating. port: 6013 dir: /home/claypool/dsh Socket created! Accepting connections. Connection request received. forked child received: john password ok command: ls executing command... Connection request received. forked child received: john password ok command: ls -l executing command... Connection request received. forked child received: john password ok command: cat Makefile executing command...
The client (login name john) from the same session:
claypool 49 capricorn% ./dsh -h distributed shell client usage: dsh [flags] <command>, where flags are: {-c command} command to execute remotely {-s host} host server is on [-p #] port server is on (default is 6013) [-h] this help message claypool 41 capricorn% ./dsh -c "ls" -s ccc.wpi.edu Makefile client.c dsh dsh.c index.html server server.c server.h sock.c sock.h claypool 42 capricorn% ./dsh -c "ls -l" -s ccc.wpi.edu total 37 -rw-r----- 1 claypool users 212 Nov 7 22:19 Makefile -rw-r----- 1 claypool users 997 Nov 1 09:27 client.c -rwxrwx--- 1 claypool users 6918 Nov 9 00:04 dsh -rw-r----- 1 claypool users 3790 Nov 9 00:03 dsh.c -rw-r----- 1 claypool users 5374 Nov 8 23:50 index.html -rwxrwx--- 1 claypool users 7919 Nov 9 00:09 server -rw-r----- 1 claypool users 4383 Nov 9 00:09 server.c -rw-r----- 1 claypool users 240 Nov 7 22:19 server.h -rw-r----- 1 claypool users 2638 Nov 1 09:36 sock.c -rw-r----- 1 claypool users 614 Nov 1 09:27 sock.h claypool 43 capricorn% ./dsh -c "cat Makefile" -s ccc.wpi.edu # # # CC = gcc all: server dsh server: server.c server.h $(CC) -o server server.c dsh: dsh.c server.h $(CC) -o dsh dsh.c sock.o: sock.c sock.h $(CC) -c sock.c clean: /bin/rm -f dsh server core *.o *~
You must use a Makefile for this project (it is good for you and good
for us since it makes grading easier). The program make
is a useful program for maintaining large programs that have been
broken into many software modules is make. The program uses
a file, usually named Makefile, that resides in the same
directory as the source code. This file describes how to compile a
number of targets specified. To use, simply type "make" at the
command line. WARNING: The operation line(s) for a target MUST begin
with a TAB (do not use spaces). See the man page for
make
and gcc
for additional information.
# # Possible Makefile for distributed shell program # CC = gcc CFLAGS = -Wall LIBFLAGS = all: dsh server server: server.c $(CC) $(CFLAGS) server.c server.o -o server $(LIBFLAGS) dsh: dsh.c $(CC) $(CFLAGS) dsh.c dsh.o -o dsh $(LIBFLAGS) clean: /bin/rm -rf *.o core dsh server
For you client-server shell, the main information you need is:
Also, be sure to have your experiment writeup in a clearly labeled file. Use either text, Microsoft Word, postscript or pdf format.
If you do not have your files on the CCC machines, then copy your
entire working directory to your CCC account. Then, login to the CCC
machines (using slogin
or putty
).
Use tar
with gzip
to archive your files.
For example:
mkdir lastname-proj2 cp * lastname-proj2 /* copy all the files you want to submit */ tar czvf proj2-lastname.tgz lastname-proj2 /* compress */Submit your assignment (
proj2-lastname.tgz
):
/cs/bin/turnin submit cs4513 project2 proj2-lastname.tgz
Not needed, but you can verify your submission via:
/cs/bin/turnin verify cs4513 project2
If you need more information, see Using the turnin Program for additional help with turnin.
A grading guide shows the point breakdown for the individual project components. A more general rubric follows:
100-90. Both server and client function in the specified way. Each utility is robust in the presence of errors, whether from system or user. Code builds and runs cleanly without errors or warnings. Experiments effectively test all required measurements. Experimental writeup has the three required sections, with each clearly written and the results clearly depicted.
89-80. Both the server and client meet most of the specified requirements, but a few features may be missing. Programs are mostly robust in the face of most errors, failing only in a few cases. Code builds cleanly, and runs mostly without errors or warnings. Experiments mostly test all required measurements. Experimental writeup has the three required sections, with details on the methods used and informative results.
79-70. Both serer and client are in place, and the client can connect to the server, but core shell functionality is missing. Simple, one-word commands may work properly, but little more complicated. Code compiles, but may exhibit warnings. Programs may fail ungracefully under some conditions. Experiments are incomplete and/or the writeup does not provide clarity on the methods or results.
69-60. Server and client do not function consistently in tandem. Code parts may be in place but do not work properly. Code compiles, but may exhibit warnings. Programs may fail ungracefully under many conditions. Experiments are incomplete and the writeup does not provide clarity on the methods or results.
59-0. Server and client may connect but do little else properly. Code do handle parts of the client/server functionality may be there, but does not function properly. Code may not even compiles without fixes. Experiments are incomplete with a minimal writeup.
Description | Examples | Makefiles | Experiments | Hints | Hand In | Grading
Send all questions to the TA mailing list (cs4513-staff at cs.wpi.edu).