Client / Server and Distributed Programming

Client / server programming in the Unix environment is almost as easy as ordinary programming. To do client / server programming, you need to define a protocol which your client and server will use for communication. This protocol is spoken across ordinary file descriptors, using ordinary read and write commands (gets and puts for Tcl). Only a few new commands must be used: one command to associate a network connection with file descriptors, one command for the server to identify the connecting client, and one command to handle network timeouts.

Why Client / Server?

Client / server programming is important for several reasons:

Designing A Protocol

The protocol used by your programs should have several characteristics: Examples of simple, stateless protocols include HTTP (the World Wide Web native protocol) and Gopher; simple stateful protocols include FTP, SMTP (the Simple Mail Transfer Protocol) and NNTP (the Network News Transfer Protocol).

Writing A Client

A client needs to open a network connection (usually two-way) to a server, and then communicate with it in the proper protocol via file descriptors. The client may need to worry about network delays, but this is usually less important than it is for the server.

The server_open Command

server_open ?-nobuf? host service
The server_open command takes two parameters, host, which must be either an Internet hostname or Internet address, and service, which must be either a Unix service name or an Internet port number. Service names are mapped to port numbers via the file /etc/services; here is an excerpt:
echo            7/tcp
echo            7/udp
daytime         13/tcp
daytime         13/udp
telnet          23/tcp
smtp            25/tcp          mail
http            80/tcp                          # World Wide Web
finger          79/tcp
nntp            119/tcp         usenet          # Network News Transfer
So for example, an HTTP connection to CERN might be initiated with any of these equivalent commands:
server_open info.cern.ch http
server_open info.cern.ch 80
server_open 128.141.201.214 http
server_open 128.141.201.214 80
server_open returns a list of two open file descriptors, the first for reading and the second for writing. These file descriptors are buffered. (The -nobuf options returns a single unbuffered file descriptor, opened for reading and writing.) I usually use the Extended Tcl lassign command with server_open:
lassign [server_open info.cern.ch http] r w
An error is signalled if the connection fails, so one often uses catch as well; also, because long network delays can cause your program to hang for some time, I usually use the timeout function from the last lecture:
if [catch {lassign [timeout 30 {server_open info.cern.ch http}] r w} result] {
    puts stderr "Warning: $result"
}

Writing A Server

Writing a server is a little more complicated than writing a client, because a server has to hang around listening for network connections and then fork off server processes to handle the connections. Also, each server daemon (Unix jargon for the process that hangs around listening for connections) takes up virtual memory even when its not servicing a connection.

The Unix inetd program solves these problems. It's a single daemon that listens for connections on several ports at once; when a connection comes in, it forks off the appropriate server and connects the server's standard input and standard output connected to the network connection. Thanks to the inetd abstraction, servers are just as easy to write as clients.

The fstat remotehost Command

fstat fileID remotehost
To implement security, a server may want to know the name or IP address of the machine that's connecting to it (to implement access restrictions). The fstat remotehost command, when invoked with a network file descriptor, returns a list of two elements, the network address of the remote host, and (if available) the hostname.

The select Command

select readfileIds ?writefileIds? ?exceptfileIds? ?timeout?
Opening a network connection can take a long time, and may even hang for a while before failing. My timeout command can be used with server_open to solve the problem. However, network reads and writes are another story.

Its very important for a server to be able to timeout network I/O, because when the server is tied up it affects all the clients (whereas when a client is hung it affects no one else).

Network reads and writes can't be timed out with my timeout command because it uses signals, and when a read or write system call is interrupted by a signal it is automatically restarted! This is usually what you want, but not always in the case of network I/O. In C, you have control over this restarting behavior, but not in Tcl, so we need another solution.

The solution is the select system call, which takes open file descriptors as arguments and reports whether or not they are ready for reading or writing. So you use select before attempting to read or write. If select says there's data to be read, you won't hang:

# server code
while 1 {
    lassign [select [list stdin] {} {} 30] rfds _ _
    if [lempty $rfds] {
        error "Timeout!"
    }
    gets $stdin line
    ...
}
select takes four arguments. The timeout argument is a number of seconds (fractional) after which select will timeout; in this case, select returns an empty list. The readfileIds argument is a list of file descriptors which we would like to read from; the writefileIds argument is a list of file descriptors we would like to write to; and the exceptfileIds is a list of file descriptors we are interested in checking for exceptional conditions.

If select doesn't timeout, that means that one of the file descriptors is ready to be read or written. select returns a list of three elements: a list of the file descriptors ready for reading, a list of the file descriptors ready for writing, and a list of file descriptors for which some exceptional condition occurred (these are error conditions).

Distributed Programming

So far we've discussed I/O-based client / server programming, but there's another more powerful approach, called remote procedure call (RPC). The idea is simple: instead of using I/O to communicate between client and server, you simply call procedures which execute remotely!

Suppose, for example, that you need to add two numbers together, but you don't want to do it on your local machine. Using the I/O-based approach, you'd need to design a protocol to pass this addition message from client to server, write your message, and read your result. But suppose you could just invoke expr in a Tcl interpreter running on a remote machine?

The Tcl Distributed Programming extension (Tcl-DP) does just this. The client would be written as follows:

set server [dp_MakeRPCClient $serverhost $port]
dp_RPC expr 4 + 1
=> 5
The dp_MakeRPCClient command makes this interpreter an RPC client of the server at serverhost and port. The dp_RPC command executes any Tcl command in the remote interpreter.

The server only needs to source any necessary Tcl code and declare itself a sever:

source server-code.tcl
dp_MakeRPCServer 4545

Tcl-DP also supports asynchronous RPC with callbacks and distributed objects.

Security


Keith Waclena
The University of Chicago Library
This page last updated: Tue Aug 30 14:42:38 CDT 1994
This page was generated from Extended HTML by xhtml.