;# ;# Copyright (c) 1995-1998 ;# Ikuo Nakagawa. All rights reserved. ;# ;# Redistribution and use in source and binary forms, with or without ;# modification, are permitted provided that the following conditions ;# are met: ;# ;# 1. Redistributions of source code must retain the above copyright ;# notice unmodified, this list of conditions, and the following ;# disclaimer. ;# 2. Redistributions in binary form must reproduce the above copyright ;# notice, this list of conditions and the following disclaimer in the ;# documentation and/or other materials provided with the distribution. ;# ;# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ;# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE ;# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ;# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS ;# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, ;# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT ;# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR ;# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, ;# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE ;# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, ;# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ;# ;# $Id: TCP.pm,v 1.20 1998/09/19 03:58:35 ikuo Exp $ ;# ;# Description: ;# TCP.pm - TCP Class definitions ;# - Socket based operations. ;# - Multiple addresses for a single server are supported. ;# package Fan::TCP; use strict; use vars qw($VERSION $LOG $seq_id $sent_octets $recv_octets); use Carp; use Socket; use Socket6; use AutoLoader 'AUTOLOAD'; $VERSION = '0.03'; ;# BEGIN { $LOG = 5; $seq_id = 0; $sent_octets = 0; $recv_octets = 0; } ;# status report... END { &status_report if $LOG >= 6; } ;# A special marker for AutoSplit. 1; __END__ ;# Show statistics report... sub status_report ($) { my $this = shift; my $s = $sent_octets + 0; my $r = $recv_octets + 0; 1 while $s =~ s/(\d+)(\d\d\d)/$1,$2/; 1 while $r =~ s/(\d+)(\d\d\d)/$1,$2/; my $len = 0; $len = length($s) if $len < length($s); $len = length($r) if $len < length($r); warn("TCP status summary report:\n"); warn(" total $seq_id objects created\n"); warn(" sent". '.' x ($len + 3 - length($s)) . "$s octets\n"); warn(" recv". '.' x ($len + 3 - length($r)) . "$r octets\n"); } ;# Constants - a very simple routine sub FATAL () { 0x5555; # magic number... } ;# Destroy a TCP object. sub DESTROY ($) { my $self = shift; # close and leave socket information $self->close; # count up... $sent_octets += $self->{tcp_sent_octets}; $recv_octets += $self->{tcp_recv_octets}; carp("TCP DESTROYING $self") if $LOG > 5; } ;# Creat a new TCP object. ;# ;# tcp_bindaddr => undef ;# tcp_port => undef ;# tcp_host => undef ;# tcp_timeout => 120 ;# sub new ($%) { my $this = shift; my $class = ref($this) || $this; my %params = @_; my $self = \%params; # setup default values... $self->{tcp_timeout} = 120 if !defined($self->{tcp_timeout}); $self->{tcp_state} = 1; # initial status is OK $self->{tcp_error} = ''; # error message will be stored. $self->{tcp_sent_octets} = 0; $self->{tcp_recv_octets} = 0; # bless me. bless $self, $class or return undef; # count up sequence # $seq_id++; # log message carp("TCP CREATING $self") if $LOG > 5; # return myself. return $self; } ;# Show statistics report... sub stats ($) { my $self = shift; my $s = $self->{tcp_sent_octets} + 0; my $r = $self->{tcp_recv_octets} + 0; 1 while $s =~ s/(\d+)(\d\d\d)/$1,$2/; 1 while $r =~ s/(\d+)(\d\d\d)/$1,$2/; my $len = 0; $len = length($s) if $len < length($s); $len = length($r) if $len < length($r); warn("$self status report:\n"); warn(" sent". '.' x ($len + 3 - length($s)) . "$s octets\n"); warn(" recv". '.' x ($len + 3 - length($r)) . "$r octets\n"); } ;# $tcp->clearerror trys clear error flag, or ;# $tcp->clearerror(1) force to clear error flag. ;# sub clearerror ($;$) { my $self = shift; my $force = @_ && shift(@_) ? 1 : 0; return undef if !$force && !defined($self->{tcp_state}); $self->{tcp_state} = 1; # status is o.k. $self->{tcp_error} = ''; # no error message 1; } ;# TCP object's error message at the last operation. ;# sub error ($;$$) { my $self = shift; if (@_) { $self->{tcp_error} = shift; $self->{tcp_state} = @_ && (shift == &FATAL) ? undef : 0; } $self->{tcp_error}; } ;# Returns 1 if this TCP object has fatal error status. sub fatal ($) { my $self = shift; if (@_) { my $force = shift; $self->{tcp_state} = undef if $force; } defined($self->{tcp_state}) ? 0 : 1; } ;# refer or change current status of an object. ;# status may have OK, ERROR, FATAL. sub status ($;$) { my $self = shift; # once status was undefined, you can't clear it. return undef if !defined($self->{tcp_state}); # `change status' or `refer status' ? $self->{tcp_state} = shift if @_; # result is the current status. $self->{tcp_state}; } ;# dump all key/val pairs. ;# for debug purpose only. sub dump ($@) { my $self = shift; my @index = @_ ? @_ : sort keys %{$self}; my $count = 0; print("TCP dump $self"); for my $key (@index) { if (exists($self->{$key})) { print(" $key => $self->{$key}\n"); $count++; } } $count; } ;# Send a line with CR/LF. sub putln ($$) { my $self = shift; my $sock = $self->handle; my $line = shift; my $ok = 0; # DEBUG only - check socket! defined($sock) or confess("TCP: socket is not defined"); # try to clear current status. $self->clearerror || return undef; # remove trailing spaces. DO NOT in this version. # $line =~ s/\s+$//; # print a line with CR/LF. eval { local $SIG{'ALRM'} = sub { die("alarm\n") }; alarm($self->{tcp_timeout}); $ok = print $sock ($line."\r\n"); alarm(0); }; # check result. if ($@) { if ($@ eq "alarm\n") { warn("putln: TIME OUT\n") if $LOG > 5; $self->error("operation timed out", &FATAL); return undef; } croak($@); # other evaluation error } # check result. unless ($ok) { my $e = $!.''; return $self->error($e, &FATAL); warn("TCP putln($line): $e\n") if $LOG > 6; carp("$self: print - $e"); return undef; } # debug log if ($LOG > 6 || $self->{tcp_debug}) { my $fno = fileno($self->{tcp_sock}); warn("$self [$fno] putln: $line\n"); } # count up sent data size $self->{tcp_sent_octets} += length($line) + 2; # success to put lines. 1; } ;# recieve one line respone from server. sub getln ($) { my $self = shift; my $sock = $self->handle; my $line = undef; # DEBUG only - check socket! defined($sock) or confess("TCP: socket is not defined"); # try to clear current status. $self->clearerror || return undef; # if we already found end-of-file, return undef return undef if $self->{endoffile}; # get a line from socket eval { local $SIG{'ALRM'} = sub { die("alarm\n") }; alarm($self->{tcp_timeout}); $line = <$sock>; alarm(0); }; # check result. if ($@) { if ($@ eq "alarm\n") { warn("getln: TIME OUT\n") if $LOG > 5; $self->error("operation timed out", &FATAL); return undef; } carp($@); # other evaluation error } # check result unless (defined($line)) { # this is not an error warn("TCP getln: END-OF-FILE detected.\n") if $LOG > 6; $self->{endoffile}++, return undef; } # or success to read a line chomp($line); $line =~ s/\r?$//; # debug log if ($LOG > 6 || $self->{tcp_debug}) { my $fno = fileno($self->{tcp_sock}); warn("$self [$fno] getln: $line\n"); } # count up sent data size $self->{tcp_recv_octets} += length($line) + 2; # success, and return this line. $line; } ;# send data to server sub putdata ($$) { my $self = shift; my $data = shift; my $length = length($data); my $sock = $self->handle; my $ok = undef; # DEBUG only - check socket! defined($sock) or confess("TCP: socket is not defined"); # try to clear current status. $self->clearerror || return undef; # if we already found end-of-file, return undef return undef if $self->{endoffile}; # get a line from socket eval { local $SIG{'ALRM'} = sub { die("alarm\n") }; alarm($self->{tcp_timeout}); $ok = print $sock ($data); alarm(0); }; # check result. if ($@) { if ($@ eq "alarm\n") { warn("putdata: TIME OUT\n") if $LOG > 5; $self->error("operation timed out", &FATAL); return undef; } croak($@); # other evaluation error } # check result unless ($ok) { my $e = $!.''; $self->error($e, &FATAL); carp("$self: print - $e"); return undef; } # or success to send data if ($LOG > 6 || $self->{tcp_debug}) { my $fno = fileno($self->{tcp_sock}); warn("$self [$fno] wrote $length octets.\n"); } # count up sent data size $self->{tcp_sent_octets} += $length; # success, and return this line. 1; } ;# recv data to server sub getdata ($$) { my $self = shift; my $length = shift; my $sock = $self->handle; my $data = ''; my $len = 0; my $ok = undef; # DEBUG only - check socket! defined($sock) or confess("TCP: socket is not defined"); # try to clear current status. $self->clearerror || return undef; # get a line from socket eval { local $SIG{'ALRM'} = sub { die("alarm\n") }; alarm($self->{tcp_timeout}); $len = read($sock, $data, $length); alarm(0); }; # check result. if ($@) { if ($@ eq "alarm\n") { warn("getdata: TIME OUT\n") if $LOG > 5; $self->error("operation timed out", &FATAL); return undef; } croak($@); # other evaluation error } # check result unless (defined($len) && $len > 0) { $self->{endoffile}++, return undef; } # or success to recv data if ($LOG > 6 || $self->{tcp_debug}) { my $fno = fileno($self->{tcp_sock}); warn("$self [$fno] read $len octets.\n"); } # count up sent data size $self->{tcp_recv_octets} += $len; # success, and return this line. $data; } ;# sub nowait ($) { my $self = shift; my $sock = $self->handle; defined($sock) || return undef; my $a = select($sock); $| = 1; select($a); 1; } ;# opening socket... ;# and if bindport / bindaddr was specified, we try to ;# bind the socket. ;# this should be a internal routine. sub open_socket ($%) { my $self = shift; my %params = @_; # try clear error first. $self->clearerror || return undef; # if we already have a socket, close it first. $self->close; # check local side port #. my $port = $params{tcp_bindport} || $self->{tcp_bindport} || 0; # define local side address if bindaddr is not null string. my $family = $params{tcp_family} ? $params{tcp_family} : AF_INET; my $tcp_bindaddr = $params{tcp_bindaddr} || $self->{tcp_bindaddr} || (($family == AF_INET) ? '0.0.0.0' : '::'); my ($socktype, $proto, $me, $canonname); ($family, $socktype, $proto, $me, $canonname) = getaddrinfo($tcp_bindaddr, $port, $family, SOCK_STREAM); # local file handle... local *SOCKET; # creating a stream socket. unless (socket(SOCKET, $family, $socktype, $proto)) { my $e = $!.''; $self->error($e, &FATAL); carp("$self: socket - $e") if $LOG >= 5; return undef; } # bind addresses. unless (bind(SOCKET, $me)) { my $e = $!.''; $self->close; $self->error($e, &FATAL); carp("$self: bind - $e") if $LOG >= 5; return undef; } # debug log... if ($LOG > 5 || $self->{tcp_debug}) { warn("$self [".fileno(SOCKET)."] was opened.\n"); } # save it $self->{tcp_sock} = *SOCKET; # success to create and bind socket. *SOCKET; } ;# connecting the server. sub do_client ($%) { my $self = shift; my %params = @_; my $sock; # close handle if exists $self->close; # clear error or return $self->clearerror(1); # parse argument my $port = $params{tcp_port} || $self->{tcp_port}; my $host = $params{tcp_host} || $self->{tcp_host}; # check required parameters if ($port eq '') { $self->error("no tcp_port", &FATAL); carp("$self: tcp_port not defined"); return undef; } if ($host eq '') { $self->error("no tcp_host", &FATAL); carp("$self: tcp_host not defined"); return undef; } # check server name and try to parse port number my @infos = getaddrinfo($host, $port, AF_UNSPEC, SOCK_STREAM); if ($#infos < 1) { carp("$self: getaddrinfo($host, $port) - $?"); my $e = $?.''; $self->error($e, &FATAL); carp("$self: getaddrinfo($host, $port) - $e"); return undef; } # Perl's bug? once connect fails, we could not any more # connect (connect returns "Invalid Argument"). So we # create/close a socket in each iteration. while ($#infos >= 1) { my ($family, $socktype, $proto, $peer, $canonname) = splice(@infos, 0, 5); # open socket stores any error $params{tcp_family} = $family; $self->open_socket(%params) || return undef; my $result = undef; # do real work. eval { local $SIG{'ALRM'} = sub { die("alarm\n") }; alarm($self->{tcp_timeout}); $result = connect($self->{tcp_sock}, $peer); alarm(0); }; # check result. if ($result) { $self->nowait; # let this socket non-blocking warn("$self connect ok, local=" .$self->sockname.", remote=".$self->peername."\n") if $LOG > 5 || $self->{tcp_debug}; return 1; # success } if ($@) { if ($@ ne "alarm\n") { croak($@); # other evaluation error } warn("do_client: TIME OUT\n") if $LOG > 5; $self->error("operation timed out"); # not fatal. } else { # this is not a critical error, yet. my $e = $!.''; $self->error($e); carp("$self: connect - $e") if $LOG >= 6; } # perhaps, this is a perl's bug... $self->close; # or error found. } # or all connect were failed. # carp("all connect sessions were failed"); $self->error($self->error, &FATAL); undef; } ;# ;# open accept socket, and listen at specified addr/port. ;# sub do_server ($%) { my $self = shift; my %param = @_; # close handle if exists $self->close; # clear error first $self->clearerror(1); # default backlog is 5. my $backlog = $param{tcp_backlog} > 0 ? $param{tcp_backlog} : 5; # opening new socket. $self->open_socket(%param) || return undef; # try real work unless (listen($self->{tcp_sock}, $backlog)) { my $e = $!.''; $self->error($e, &FATAL); carp("$self: listen - $e"); return undef; } # success to listen 1; } ;# ;# Accept a new connection at listening socket, ;# and create a new TCP object. ;# sub new_client ($) { my $self = shift; my $sock; defined($sock = $self->handle) || return undef; my $client = $self->new; # client = new TCP object. my $result = 0; local *SOCKET; eval { local $SIG{'ALRM'} = sub { die("alarm\n") }; alarm($self->{tcp_timeout}); $result = accept(SOCKET, $sock); alarm(0); }; # check result. if ($@) { if ($@ eq "alarm\n") { warn("new_client: TIME OUT\n") if $LOG > 5; $self->error("operation timed out", &FATAL); return undef; } croak($@); # other evaluation error } # result of accept unless ($result) { my $e = $!.''; $self->error($e, &FATAL); carp("accept: $e"); return undef; } # store... $client->{tcp_sock} = *SOCKET; # debug log... if ($LOG > 5 || $self->{tcp_debug}) { my $fno = fileno($client->{tcp_sock}); warn("$self [$fno] was accepted.\n"); } # set no wait $client->nowait; $client; } ;# sub close ($) { my $self = shift; my $sock = $self->handle; # close socket if already we have opened. if (defined($sock)) { my $fno = fileno($sock); shutdown($sock, 2); CORE::close($sock); # debug log... if ($LOG > 5 || $self->{tcp_debug}) { warn("$self [$fno] was closed.\n"); } } # delete socket file handle delete($self->{tcp_sock}); # 1; } ;# sub handle ($) { my $self = shift; # check existence of tcp_sock - we must have this. unless (exists($self->{tcp_sock})) { carp("$self has no tcp_sock") if $LOG > 6; return undef; } # copy to a local variable. my $s = $self->{tcp_sock}; # validate our socket. unless (defined($s) && defined(fileno($s))) { carp("$self->tcp_sock is not a file handle") if $LOG > 7; return undef; } # DEBUG information. if ($LOG > 7) { my $fno = fileno($s); warn("$self->tcp_sock=[$fno]\n"); } # return file handle itself. $s; } ;# sub sockname ($) { my $self = shift; my $sock = $self->handle; if (defined($sock)) { my $sa = getsockname($sock); my $family = (unpack('CC', $sa))[1]; my ($addr, $port) = getnameinfo($sa, NI_NUMERICHOST | NI_NUMERICSERV); carp("$self sockname=$addr|$port|$family") if $LOG > 7; return wantarray ? ($port, $addr, $family) : "$addr|$port|$family"; } return wantarray ? () : undef; } ;# sub peername ($) { my $self = shift; my $sock = $self->handle; if (defined($sock)) { my $sa = getpeername($sock); my $family = (unpack('CC', $sa))[1]; my ($addr, $port) = getnameinfo($sa, NI_NUMERICHOST | NI_NUMERICSERV); carp("$self sockpeer=$addr|$port|$family") if $LOG > 7; return wantarray ? ($port, $addr, $family) : "$addr|$port|$family"; } return wantarray ? () : undef; } ;# end of Fan::TCP module