next up previous
Next: 3.3 Defensive Servers Up: 3. Internet Sockets Previous: 3.1 Clients and Servers

Subsections


  
3.2 Concurrent Servers

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.

  
3.2.1 Naïve Server

The first version of the line-length server does not even check for errors: *

-- Line-length server, version 1 (naïve)
 
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;
This server will indeed serve any number of clients simultaneously, subject only to system resource availability, but it has some problems.

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.

  
3.2.2 Shell-Aided Server

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)
 
server_fd := open (`54002', `server-socket');                -- listen
loop                                             -- cycle indefinitely
  fd := accept (server_fd);                -- accept client connection
  if fd /= om 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;
The program lengths.setl that is run in the background for each client is as follows: *
-- ``lengths.setl''
 
-- 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;
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.

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.

  
3.2.3 Shell-Independent Server

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)
 
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_fdsigchld_fd}]);
 
  if server_fd in ready then                 -- client wants to connect
    fd := accept (server_fd);                      -- accept connection
    if fd /= om 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 (stderrchild_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 (stderrchild_pid, `rc =', status, `at', date);
  end if;
 
end loop;
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.

  
3.2.4 Pump-Aided Server

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)
 
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 fd /= om then                                          -- got it
      pump_fd := pump();
      if pump_fd = -1 then                             -- child process
        -- Deal with client through fd
        dup2 (stdoutstderr);           -- 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_fd /= om then                        -- child was created
        printa (stderrpid (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 (stderrchild_pid, `:', pretty child_output,
                       `rc =', status, `at', date);
    pumps less:= pump_fd;              -- remove pump_fd from pumps
  end loop;
 
end loop;
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.


next up previous
Next: 3.3 Defensive Servers Up: 3. Internet Sockets Previous: 3.1 Clients and Servers
David Bacon
1999-12-10