Except when the purpose of a server is to serialize access to some resource, it should be available to clients at all times. For example, a public Web server ought to be able to manage several connections simultaneously, and hand off most of the responsibility for clients to some equivalent of separate threads or processes so as to be able to accept new clients quickly.
In this section, a running example called the line-length server is introduced. Its actual function is quite trivial, in order not to obscure the issues confronting a server involved in extended interactions with multiple clients. For each line of text it reads from a client, the line-length server simply replies with a number indicating how many characters long that input line was. The number is itself formatted as a run of decimal digit characters on a single line.
The first version of the line-length server does not even check for errors: *
-- Line-length server, version 1 (naïve)This server will indeed serve any number of clients simultaneously, subject only to system resource availability, but it has some problems.
server_fd := open (`54001', `server-socket'); -- listen
loop -- cycle indefinitely
fd := accept (server_fd); -- accept client connection
if fork() = 0 then
-- Child process; deal with client through fd
while (line := getline fd)om loop
printa (fd, #line); -- number of chars in line
end loop;
stop; -- normal exit from child process
end if;
-- Parent continues here
close (fd); -- child copy of fd stays open as long as necessary
end loop;
First, there is a subtle consequence of the fact that the file descriptor ostensibly returned by accept is not checked for being om. On rare occasions, accept will unblock because of an incoming connection request, and then, due to any number of network hazards, fail to establish the TCP connection. The child process will crash on its first attempt to use om as a file descriptor, which is unfortunate but does not affect the parent. However, the parent itself will crash when it calls close on om, rendering the whole service unavailable. We will solve this problem simply by conditioning everything after the accept call on fd not being om.
The second problem with this first version of the line-length server is that on Unix systems, where child processes can return a status code to their parents, the operating system is required to keep a record of the status code until the parent asks for it using wait or one of its variants. If we start the server as shown, have some clients use it and close their connections, and then ask ps to tell us about our processes, it will list all child processes that have finished interacting with clients (and hence exited) as zombies, the technical term for processes that have terminated but not yet had their status codes ``reaped'' by wait. Eventually, the system will not be able to allocate any more child processes, fork will start consistently returning om, and all further clients will be dropped immediately after they have been accepted. We will see in Section 3.2.3 [Shell-Independent Server] how to be informed of when to call wait, the SETL reflection of wait that was described in Section 2.17.2 [Processes], in order to clear zombies from the process table (which is of finite size).
The problem underlying the need for this compulsory housekeeping of calling wait is that fork itself is an unnecessarily low-level function. Fork was listed among the Posix interface routines in Section 2.17.2 [Processes], and is really intended for system-level, not application-level work. There is almost always a better way of starting child processes in SETL using higher-level facilities such as system, filter, or a pump stream as previously described.
For example, we can fix the above problems by checking fd and using system to start child processes in the ``background'': *
-- Line-length server, version 2 (shelly)The program lengths.setl that is run in the background for each client is as follows: *
server_fd := open (`54002', `server-socket'); -- listen
loop -- cycle indefinitely
fd := accept (server_fd); -- accept client connection
if fdom then
-- Convert fd to string, form command with it, run in background
system (`setl lengths.setl -- ' + str fd + ` &');
close (fd); -- this has been inherited by the background task
end if;
end loop;
-- ``lengths.setl''The file descriptor is inherited by this child program, and identified on the command line that starts the child. In the child, the string token is converted using val, and the resulting integer is opened as a bidirectional SETL stream using the `rw' mode. The trailing ampersand on the command invocation in the server is standard shell syntax to indicate that the shell should run the command in the background, i.e., as an independent, concurrent process that does not automatically receive keyboard-generated signals even if its (foreground) parent does.
-- Convert command-line parameter to integer, open r/w stream over it
fd := open (val command_line(1), `rw');
while (line := getline fd)om loop
printa (fd, #line); -- line length
end loop;
Because system uses the shell to run commands, the executing instances of the child program in the above example are not direct children of the server, but rather of the shell, which exits (returns to the caller of system) immediately after launching the background process. In Unix, such ``orphaned'' processes automatically become children of the permanently resident init process, which then also takes over responsibility for reaping their status codes and thereby clearing them from the operating system's process table when they terminate.
If we wanted to avoid the use of the shell, perhaps on the grounds of a weak performance argument, syntactic allergy, or dependency paranoia, and happened to be familiar with the Posix API, we could code the line-length server in much the same way as it would be done in C or Perl, in contempt of the high-level approach. The following version does just that, and adds logging on stderr as a feature: *
-- Line-length server, version 3 (posixy)Here we are also checking server_fd for om. This was not necessary in previous versions of the server, because the server would have crashed in an immediate and obvious way when the om value was passed to accept. Here, however, the om would enter silently into the set passed to select (the I/O event-waiting routine introduced in Section 2.5 [Multiplexing with Select]), and the program would simply sleep indefinitely, waiting for a CHLD signal through the remaining singleton set containing just sigchld_fd.
const server_port = `54003';
server_fd := open (server_port, `server-socket'); -- listen
if server_fd = om then
-- Cannot get server port
printa (stderr, `Port', server_port, `-', last_error);
stop 1; -- exit with status code = 1
end if;
-- Arrange to receive CHLD (child exit) signals
sigchld_fd := open (`CHLD', `signal');
loop -- cycle indefinitely
-- Wait for listener and/or signal input
[ready] := select ([{server_fd, sigchld_fd}]);
if server_fd in ready then -- client wants to connect
fd := accept (server_fd); -- accept connection
if fdom then -- got it
child_pid := fork();
if child_pid = 0 then -- child process
-- Deal with client through fd
while (line := getline fd)om loop
printa (fd, #line); -- line length
end loop;
stop; -- exit with status code = 0
end if;
-- Parent or only process continues here
close (fd); -- child uses fd
printa (stderr, child_pid, `started at', date);
end if;
end if;
if sigchld_fd in ready then -- a child process has exited
line := getline sigchld_fd; -- take the signal
child_pid := wait(); -- get child process id and exit status
printa (stderr, child_pid, `rc =', status, `at', date);
end if;
end loop;
If more elaborate communication between parent and child were desired, such as a reporting of the number of lines and characters served, the hardy Posix API enthusiast might even go so far as to code the appropriate pipe, dup2, and close calls. But it is much easier to let the pump primitive take care of all such low-level housekeeping. In the following version of the line-length server, a set is used to keep track of all the pump file descriptors. There is no need to catch CHLD signals any more, because child termination is reflected as an end-of-file condition on the child's pump stream, and the compulsory wait is implicit in the close then applied to that stream's file descriptor: *
-- Line-length server, version 4 (pumpy)The purpose of pretty here is to ensure that the child's output is a legible part of the final printa statement about that child. For normal output, this will just be the report of lines and characters, as a quoted string. If the child crashes, as could happen if the client closes the connection without reading the reply to the last line it sends the server, this ``pretty'' string will contain any diagnostic output that might appear on either stdout or, because of the dup2 call (which creates aliases as described in Section 2.17.1 [I/O and File Descriptors]), stderr.
const server_port = `54004';
server_fd := open (server_port, `server-socket'); -- listen
if server_fd = om then
-- Cannot get server port
printa (stderr, `Port', server_port, `-', last_error);
stop 1; -- exit with status code = 1
end if;
pumps := {}; -- pump stream file descriptors
loop -- cycle indefinitely
-- Wait for listener and/or pump stream input
[ready] := select ([{server_fd} + pumps]);
if server_fd in ready then -- client wants to connect
fd := accept (server_fd); -- accept connection
if fdom then -- got it
pump_fd := pump();
if pump_fd = -1 then -- child process
-- Deal with client through fd
dup2 (stdout, stderr); -- like shell 2>&1 redirection
lines := 0;
chars := 0;
while (line := getline fd)om loop
printa (fd, #line); -- line length
lines +:= 1;
chars +:= #line;
end loop;
printa (stderr, `lines =', lines, `chars =', chars);
stop; -- exit with status code 0
end if;
-- Parent or only process continues here
close (fd); -- child (if any) uses fd
if pump_fdom then -- child was created
printa (stderr, pid (pump_fd), `started at', date);
pumps with:= pump_fd; -- include pump_fd in pumps
else -- no child was created
printa (stderr, `pump() failed at', date);
end if;
end if;
end if;
for pump_fd in ready * pumps loop -- for each child with output
child_pid := pid (pump_fd); -- process id of child
child_output := getfile pump_fd; -- child's entire output
close (pump_fd); -- close the pump stream and clear the zombie
printa (stderr, child_pid, `:', pretty child_output,
`rc =', status, `at', date);
pumps less:= pump_fd; -- remove pump_fd from pumps
end loop;
end loop;