Internet TCP Clients and Servers

- Internet TCP Clients and Servers

## Internet TCP Clients and Servers

Use Internet-domain sockets when you want to do client-server communication that might extend to machines outside of your own system.

Here's a sample TCP client using Internet-domain sockets:

        #!/usr/bin/perl -w
        use strict;
        use Socket;
        my ($remote,$port, $iaddr, $paddr, $proto, $line);

        $remote  = shift || 'localhost';
        $port    = shift || 2345;  # random port
        if ($port =~ /\D/) { $port = getservbyname($port, 'tcp') }
        die "No port" unless $port;
        $iaddr   = inet_aton($remote)               || die "no host: $remote";
        $paddr   = sockaddr_in($port, $iaddr);

        $proto   = getprotobyname('tcp');
        socket(SOCK, PF_INET, SOCK_STREAM, $proto)  || die "socket: $!";
        connect(SOCK, $paddr)    || die "connect: $!";
        while (defined($line = <SOCK>)) {
            print $line;
        }

        close (SOCK)            || die "close: $!";
        exit;

And here's a corresponding server to go along with it. We'll leave the address as INADDR_ANY so that the kernel can choose the appropriate interface on multihomed hosts. If you want sit on a particular interface (like the external side of a gateway or firewall machine), you should fill this in with your real address instead.

        #!/usr/bin/perl -Tw
        use strict;
        BEGIN { $ENV{PATH} = '/usr/ucb:/bin' }
        use Socket;
        use Carp;
        my $EOL = "\015\012";

        sub logmsg { print "$0 $$: @_ at ", scalar localtime, "\n" }

        my $port = shift || 2345;
        my $proto = getprotobyname('tcp');

        ($port) = $port =~ /^(\d+)$/                        or die "invalid port";

        socket(Server, PF_INET, SOCK_STREAM, $proto)        || die "socket: $!";
        setsockopt(Server, SOL_SOCKET, SO_REUSEADDR,
                                            pack("l", 1))   || die "setsockopt: $!";
        bind(Server, sockaddr_in($port, INADDR_ANY))        || die "bind: $!";
        listen(Server,SOMAXCONN)                            || die "listen: $!";

        logmsg "server started on port $port";

        my $paddr;

        $SIG{CHLD} = \&REAPER;

        for ( ; $paddr = accept(Client,Server); close Client) {
            my($port,$iaddr) = sockaddr_in($paddr);
            my $name = gethostbyaddr($iaddr,AF_INET);

            logmsg "connection from $name [",
                    inet_ntoa($iaddr), "]
                    at port $port";

            print Client "Hello there, $name, it's now ",
                            scalar localtime, $EOL;
        }

And here's a multithreaded version. It's multithreaded in that like most typical servers, it spawns (forks) a slave server to handle the client request so that the master server can quickly go back to service a new client.

        #!/usr/bin/perl -Tw
        use strict;
        BEGIN { $ENV{PATH} = '/usr/ucb:/bin' }
        use Socket;
        use Carp;
        my $EOL = "\015\012";

        sub spawn;  # forward declaration
        sub logmsg { print "$0 $$: @_ at ", scalar localtime, "\n" }

        my $port = shift || 2345;
        my $proto = getprotobyname('tcp');

        ($port) = $port =~ /^(\d+)$/                        or die "invalid port";

        socket(Server, PF_INET, SOCK_STREAM, $proto)        || die "socket: $!";
        setsockopt(Server, SOL_SOCKET, SO_REUSEADDR,
                                            pack("l", 1))   || die "setsockopt: $!";
        bind(Server, sockaddr_in($port, INADDR_ANY))        || die "bind: $!";
        listen(Server,SOMAXCONN)                            || die "listen: $!";

        logmsg "server started on port $port";

        my $waitedpid = 0;
        my $paddr;

        use POSIX ":sys_wait_h";
        sub REAPER {
            my $child;
            while (($waitedpid = waitpid(-1,WNOHANG)) > 0) {
                logmsg "reaped $waitedpid" . ($? ? " with exit $?" : '');
            }
            $SIG{CHLD} = \&REAPER;  # loathe sysV
        }

        $SIG{CHLD} = \&REAPER;

        for ( $waitedpid = 0;
              ($paddr = accept(Client,Server)) || $waitedpid;
              $waitedpid = 0, close Client)
        {
            next if $waitedpid and not $paddr;
            my($port,$iaddr) = sockaddr_in($paddr);
            my $name = gethostbyaddr($iaddr,AF_INET);

            logmsg "connection from $name [",
                    inet_ntoa($iaddr), "]
                    at port $port";

            spawn sub {
                $|=1;
                print "Hello there, $name, it's now ", scalar localtime, $EOL;
                exec '/usr/games/fortune'           # XXX: `wrong' line terminators
                    or confess "can't exec fortune: $!";
            };

        }

        sub spawn {
            my $coderef = shift;

            unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') {
                confess "usage: spawn CODEREF";
            }

            my $pid;
            if (!defined($pid = fork)) {
                logmsg "cannot fork: $!";
                return;
            } elsif ($pid) {
                logmsg "begat $pid";
                return; # I'm the parent
            }
            # else I'm the child -- go spawn

            open(STDIN,  "<&Client")   || die "can't dup client to stdin";
            open(STDOUT, ">&Client")   || die "can't dup client to stdout";
            ## open(STDERR, ">&STDOUT") || die "can't dup stdout to stderr";
            exit &$coderef();
        }

This server takes the trouble to clone off a child version via `fork()` for each incoming request. That way it can handle many requests at once, which you might not always want. Even if you don't fork(), the `listen()` will allow that many pending connections. Forking servers have to be particularly careful about cleaning up their dead children (called ``zombies'' in Unix parlance), because otherwise you'll quickly fill up your process table.

We suggest that you use the -T flag to use taint checking (see the perlsec manpage) even if we aren't running setuid or setgid. This is always a good idea for servers and other programs run on behalf of someone else (like CGI scripts), because it lessens the chances that people from the outside will be able to compromise your system.

Let's look at another TCP client. This one connects to the TCP ``time'' service on a number of different machines and shows how far their clocks differ from the system on which it's being run:

        #!/usr/bin/perl  -w
        use strict;
        use Socket;

        my $SECS_of_70_YEARS = 2208988800;
        sub ctime { scalar localtime(shift) }

        my $iaddr = gethostbyname('localhost');
        my $proto = getprotobyname('tcp');
        my $port = getservbyname('time', 'tcp');
        my $paddr = sockaddr_in(0, $iaddr);
        my($host);

        $| = 1;
        printf "%-24s %8s %s\n",  "localhost", 0, ctime(time());

        foreach $host (@ARGV) {
            printf "%-24s ", $host;
            my $hisiaddr = inet_aton($host)     || die "unknown host";
            my $hispaddr = sockaddr_in($port, $hisiaddr);
            socket(SOCKET, PF_INET, SOCK_STREAM, $proto)   || die "socket: $!";
            connect(SOCKET, $hispaddr)          || die "bind: $!";
            my $rtime = '    ';
            read(SOCKET, $rtime, 4);
            close(SOCKET);
            my $histime = unpack("N", $rtime) - $SECS_of_70_YEARS;
            printf "%8d %s\n", $histime - time, ctime($histime);
        }

- Back to Internet Line Terminators in the perlipc manpage

- Forward to Unix-Domain TCP Clients and Servers in the perlipc manpage

- Up to the perlipc manpage

Internet TCP Clients and Servers