Recipe 17.10 Writing Bidirectional Clients

17.10.1 Problem

You want set up a fully interactive client so you can type a line, get the answer, type a line, get the answer, etc., somewhat like telnet.

17.10.2 Solution

Once you've connected, fork off a duplicate process. One twin reads only your input and passes it on to the server, and the other reads only the server's output and sends it to your own output.

17.10.3 Discussion

In a client-server relationship, it is difficult to know whose turn it is to talk. Single-threaded solutions involving the four-argument version of select are hard to write and maintain. But there's no reason to ignore multitasking solutions. The fork function dramatically simplifies this problem.

Once you've connected to the service you'd like to chat with, call fork to clone a twin. Each of these two (nearly) identical processes has a simple job. The parent copies everything from the socket to standard output, and the child simultaneously copies everything from standard input to the socket.

The code is in Example 17-4.

Example 17-4. biclient
  #!/usr/bin/perl -w
  # biclient - bidirectional forking client
  use strict;
  use IO::Socket;
  my ($host, $port, $kidpid, $handle, $line);
  
  unless (@ARGV =  = 2) { die "usage: $0 host port" }
  ($host, $port) = @ARGV;
  
  # create a tcp connection to the specified host and port
  $handle = IO::Socket::INET->new(Proto     => "tcp",
                                  PeerAddr  => $host,
                                  PeerPort  => $port)
         or die "can't connect to port $port on $host: $!";
  
  $handle->autoflush(1);              # so output gets there right away
  print STDERR "[Connected to $host:$port]\n";
  
  # split the program into two processes, identical twins
  die "can't fork: $!" unless defined($kidpid = fork( ));
  
  if ($kidpid) {                      
      # parent copies the socket to standard output
      while (defined ($line = <$handle>)) {
          print STDOUT $line;
      }
      kill("TERM" => $kidpid);        # send SIGTERM to child
  }
  else {                              
      # child copies standard input to the socket
      while (defined ($line = <STDIN>)) {
          print $handle $line;
      }
  }
  exit;

To accomplish the same thing using just one process is remarkably more difficult. It's easier to code two processes, each doing a single task, than it is to code one process to do two different tasks. Take advantage of multitasking by splitting your program into multiple threads of control, and some of your bewildering problems will become much easier.

The kill function in the parent's if block is there to send a signal to the child (currently running in the else block) as soon as the remote server has closed its end of the connection. The kill at the end of the parent's block is there to eliminate the child process as soon as the server on the other end goes away.

If the remote server sends data a byte at time and you need that data immediately without waiting for a newline (which may never arrive), you may wish to replace the while loop in the parent with the following:

my $byte;
while (sysread($handle, $byte, 1) =  = 1) {
    print STDOUT $byte;
}

Making a system call for each byte you want to read is not very efficient (to put it mildly), but it is the simplest to explain and works reasonably well.

17.10.4 See Also

The sysread and fork functions in Chapter 29 of Programming Perl and in perlfunc(1); the documentation for the standard IO::Socket module; Recipe 16.5; Recipe 16.10; Recipe 17.11