Computing: DOS, OS/2 & Windows Programming

SOAP client-server implementation using Perl: An introduction.

"SOAP, originally an acronym for Simple Object Access Protocol, is a messaging protocol specification for exchanging structured information in the implementation of web services in computer networks. It uses XML Information Set for its message format, and relies on application layer protocols, most often Hypertext Transfer Protocol (HTTP), for message negotiation and transmission."

The paragraph above is how Wikipedia describes SOAP. Here are some general characteristics of SOAP, as stated by Google Search AI:

This tutorial doesn't contain any information about how SOAP works. In fact, it's not more (and not less) than a series of scripts, that show the basics of writing simple client-server applications based on SOAP and using Perl as programming language. The scripts uses the SOAP::Lite module from CPAN; this module should normally be installed by default with Strawberry Perl.

Example 1: Simple "Hello World" client-server application.

Our first server (server1.pl) is implemented as a stand-alone daemon, listening on a specific port (I chose port 4080). It provides two callable methods, that return a short resp. a long "Hello World" message. The client (client1.pl) connects to this server, and executes the remote methods, what will result in getting the corresponding messages from the server.

Here is the code of server1.pl:

    use strict; use warnings;
    use SOAP::Transport::HTTP;
    my $server = SOAP::Transport::HTTP::Daemon
        -> new (LocalPort => 4080)
        -> dispatch_to('Hello');
    print "SOAP server URL = ", $server->url, "\n";
    $server->handle;

    package Hello;
    sub sayhello_short {
        return "Hello, World!";
    }
    sub sayhello_long {
        return "Hello, World from SOAP!";
    }

The script is made of two parts. The first part sets up a SOAP wrapper around a class, called "Hello". Everything from package Hello downward is the class being wrapped. The server uses the HTTP protocol, and is implemented as a daemon, i.e. a stand-alone server (actually listening on port 4080), that has nothing more to offer than the services defined as methods of the "Hello" class (coded as two subroutines of the package "Hello"). The dispatch() method defines how to dispatch the client request, and the handle() method does everything that has to be done in order to handle any incoming requests.

Here is the code of client1.pl:

    use strict; use warnings;
    use SOAP::Lite;
    my $client = SOAP::Lite
        -> uri("http://wk-win10:4080/Hello")
        -> proxy("http://wk-win10:4080/");
    print $client
        -> sayhello_short
        -> result;
    print "\n";
    print $client
        -> sayhello_long
        -> result;
    print "\n";

There are two methods to call in order to make possible the communication between the client and the server:

  1. proxy() is used to define the server end-point. In the case of a stand-alone HTTP-based server, this corresponds to the URL of the server document root. In the case of a server implemented as CGI script stored on a regular webserver, it corresponds to the URL of this script on the webserver.
  2. uri() is used to define the XML namespace, that identifies a unique service on this server. The part after the server URL corresponds to the name of the class, that the methods to be called belong to. In other words, the value of uri() has to be the server address plus the name of the package that the server dispatches the call to these methods to.

Note: The server URL http://wk-win10:4080 is for my Window 10 laptop, "wk-win10" being the name of that computer. You'll have to adapt the URL for your computer, of course...

After having called one of the "Hello World" methods, we can retrieve the server return value (the short or long greeting message) using the result() method.

The screenshots show the execution of the server (screenshot on the left) and the client (screenshot on the right).

SOAP server with Perl - Simple client-server application
SOAP client with Perl - Simple client-server application

Note: The unusual URL displayed as server address is actually nothing else than localhost...

Example 2: Passing arguments.

Passing arguments to a remote method using SOAP is essentially the same than passing them to a local method. On the client side, the arguments are specified as parameters of the method. On the server side the argument values are retrieved from @_.

The stand-alone HTTP server server2.pl provides a service that consists in returning a personal greeting message for the user, whose name is passed as argument. Here is the code of server2.pl:

    use strict; use warnings;
    use SOAP::Transport::HTTP;
    my $server = SOAP::Transport::HTTP::Daemon
        -> new (LocalPort => 4080)
        -> dispatch_to('Hello');
    print "SOAP server URL = ", $server->url, "\n";
    $server->handle;

    package Hello;
    sub hello_user {
        my ($class, $user) = @_;
        $user = 'World' unless ($user);
        return "Hello, $user!";
    }

The client makes 2 requests, the first without argument, the second using a hard-coded name as argument. Here is the code of client2.pl:

    use strict; use warnings;
    use SOAP::Lite;
    my $client = SOAP::Lite
        -> uri("http://wk-win10:4080/Hello")
        -> proxy("http://wk-win10:4080/");
    print $client
        -> hello_user
        -> result;
    print "\n";
    print $client
        -> hello_user("Aly")
        -> result;
    print "\n";

The screenshots below shows the output of the client.

SOAP client with Perl - Passing arguments

Example 3: Multiple return values.

The stand-alone HTTP server server3.pl provides a service that, for a DNA sequence passed as argument, returns the length of the sequence, as well as the count of the four bases A, C, G, and T (in this order). To be able to access the server not only from localhost, but also access the server from other computers on the local network, we must make the server listen on the IP address of the local network interface. In my case, this IP is 192.168.40.1.

We can specify an IP address with the new() method, when creating the server object, just as we did for the port in the examples before. The property to use is called LocalAddr. Here is the code of server3.pl:

    use strict; use warnings;
    use SOAP::Transport::HTTP;
    my $server = SOAP::Transport::HTTP::Daemon
        -> new (LocalPort => 4080,
                    LocalAddr => '192.168.40.1')
        -> dispatch_to('DNA');
    print "SOAP server URL = ", $server->url, "\n";
    $server->handle;

    package DNA;
    sub dna_count {
        my ($class, $dna) = @_;
        my ($len, $a, $c, $g, $t) = (0, 0, 0, 0, 0);
        if ($dna) {
            $len = length($dna); my $dna0;
            $dna0 = $dna; $dna0 =~ s/[^a]//gi; $a = length($dna0);
            $dna0 = $dna; $dna0 =~ s/[^c]//gi; $c = length($dna0);
            $dna0 = $dna; $dna0 =~ s/[^g]//gi; $g = length($dna0);
            $dna0 = $dna; $dna0 =~ s/[^t]//gi; $t = length($dna0);
        }
        return ($len, $a, $c, $g, $t);
    }

In the previous example, we have used the result() method to retrieve the server return. In fact, this method only allows to retrieve a single return value. If multiple values are returned, it's only the first one that is retrieved with result(); to access the rest, the paramsout() method has to be used. This method returns an array, that contains all values returned by the server, except the first one. Thus, in our sample application, we first call result() to get the sequence length, then we call paramsout(), that returns an array with the counts of the 4 bases. Here is the code of client3.pl:

    use strict; use warnings;
    use SOAP::Lite;
    my @bases = ('adenine', 'cytosine', 'guanine', 'thymine');
    print "Enter DNA sequence? ";
    my $dna = <STDIN>; chomp($dna); print "\n";
    my $soap = SOAP::Lite
        -> uri("http://wk-win10:4080/DNA")
        -> proxy("http://wk-win10:4080/")
        -> dna_count($dna);
    my $len = $soap->result;
    my @counts = $soap->paramsout;
    print "Sequence length = $len\n";
    print "Sequence bases:\n";
    for (my $i = 0; $i < scalar @bases; $i++) {
        printf('%-10s', "  $bases[$i]");
        printf('%4u', $counts[$i]);
        print "\n";
    }
    print "\n";

As I call result() before calling paramsout(), one might think that if the method returns only the base counts, it's because the DNA length has already been retrieved using result(). This seems not to be the case, the array returned by paramsout() never contains the first value returned by the server (?). This is not what I found in some examples published on the net. But, considering a service that only returns the 4 counts, and retrieving them as above with @counts = $soap->paramsout, the return function on the server must be coded as something like return(-1, $a, $c, $g, $t).

The screenshots show the execution of the server (screenshot on the left) and the client (screenshot on the right). Note, that this server listens on IP 192,168.40.1. Also note, that the client in this case ran on another computer than the server (a Windows 11 machine, in fact).

SOAP server with Perl - Multiple return values
SOAP client with Perl - Multiple return values (local network access)

Note: Access from a machine other than localhost may be blocked by your firewall. Whereas firewalls like ZoneAlarm Free normally allow all access from the local network, others, including Windows Defender Firewall, block all access. The screenshot below shows the error message displayed by the Windows 11 client, in the case where the connection is timed out, because blocked by the firewall on my Windows 10. If you intend to use the SOAP server permanently, you'll have to create a firewall rule; if it's just a try-out, you can disable the firewall, while accessing the SOAP server from another computer.

SOAP client with Perl - Client request blocked by the firewall on the target computer

Maybe you wonder why I changed the arguments of the client's proxy() and uri() methods, using the IP address instead of the computer name, as I did before. My Windows 10 computer has several (Virtual VMware) network cards, with manually assigned IP addresses. And, considering the output when performing a ping of "wk-win10" (the name of my Windows 10 computer), it seems that this name is "associated" with the IP address 192.168.41.1. Thus, using the computer name, connection with the SOAP server from the client running on Windows 10 fails. On the other hand, no problem to use the DNS name of my Windows 10 machine on Windows 11, the name resolution (with translation of "wk-win10" to IP 192.168.40.1) being handled by a DNS server, that is part of my local network.

Example 4: Web client access.

In the 3 sample scripts before, we have accessed the SOAP server from a command line client. In this example, we'll implement the client as a CGI script, that we save on an Apache webserver. And access the server by running the client script in the web browser.

The server sample server4.pl (listening at IP 192.168.40.1, port 4080) provides several date and time related services. Here is the code:

    use strict; use warnings;
    use SOAP::Transport::HTTP;
    my $server = SOAP::Transport::HTTP::Daemon
        -> new (LocalPort => 4080,
                    LocalAddr => '192.168.40.1')
        -> dispatch_to('MYDATETIME');
    print "SOAP server URL = ", $server->url, "\n";
    $server->handle;

    package MYDATETIME;
    sub get_date {
        my ($s, $n, $h, $d, $ymonth, $y1900, $wday, $yday, $isdst) = localtime();
        my $m = $ymonth + 1; my $y = 1900 + $y1900;
        return sprintf('%02u.%02u.%04u', $d, $m, $y);
    }
    sub get_date_long {
        my @months = qw( January February March April May June July August September October November December );
        my ($s, $n, $h, $d, $ymonth, $y1900, $wday, $yday, $isdst) = localtime();
        my $y = 1900 + $y1900;
        return sprintf('%u %s %04u', $d, $months[$ymonth], $y);
    }
    sub get_time {
        my ($s, $n, $h, $d, $m, $y, $wday, $yday, $isdst) = localtime();
        return sprintf('%02u:%02u:%02u', $h, $n, $s);
    }
    sub day_of_week {
        my @days_of_week = qw(Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday);
        my ($s, $n, $h, $d, $mm, $y, $wday, $yday, $isdst) = localtime();
        return $days_of_week[$wday];
    }

The client sample client4.pl is a Perl CGI script (save it into the cgi-bin directory of your Apache webserver), that uses the services of our SOAP server to display the actual date and time. Here is the code:

    #!C:/Programs/Strawberry/win64/perl/bin/perl.exe
    use strict; use warnings;
    use SOAP::Lite;
    print "Content-Type: text/html\n\n";
    print "<html><body>";
    print "<p>";
    my $soap = SOAP::Lite
        -> uri("http://192.168.40.1:4080/MYDATETIME")
        -> proxy("http://192.168.40.1:4080/");
    print "Actual date: ";
    print $soap
        ->day_of_week
        ->result;
    print ", ";
    print $soap
        ->get_date_long
        ->result;
    print "<br/>";
    print "Actual time: ";
    print $soap
        ->get_time
        ->result;
    print "</p>";
    print "</body></html>";

Here is the client output in Firefox web browser.

SOAP client with Perl - Client as Perl CGI script

Example 5: Server as CGI script.

So far, our SOAP server was implemented as a HTTP daemon, a stand-alone server, listening on a given IP and a given port. There is, however, another possibility to implement the server. As our server uses the HTTP protocol, we can implement it as a CGI-script, that we save in the CGI-bin directory of our Apache webserver. In this case, the server has to be created as a SOAP::Transport::HTTP::CGI object. No need to specify an IP, or a port; these will be the listen-to address and port of Apache.

The server sample server5.pl translates a protein sequence, given as argument of the service method, from 1-letter to 3-letter code. Here is the source:

    #!C:/Programs/Strawberry/win64/perl/bin/perl.exe
    use strict; use warnings;
    use SOAP::Transport::HTTP;
    my $server = SOAP::Transport::HTTP::CGI
        -> dispatch_to('Proteins')
        -> handle;

    package Proteins;
    sub is_valid_protein {
        my ($class, $protein) = @_; my $valid = 0;
        my $aa = 'ACDEFGHIKLMNPQRSTVWY';
        if ($protein =~ /^([$aa]+)$/) {
            $valid = 1;
        }
        return $valid;
    }
    sub protein_code3 {
        my %amino_acids = (
            'A' => { 'code3' => 'Ala', 'name' => 'Alanine' },
            'C' => { 'code3' => 'Cys', 'name' => 'Cysteine' },
            'D' => { 'code3' => 'Asp', 'name' => 'Aspartic acid' },
            'E' => { 'code3' => 'Glu', 'name' => 'Glutamic acid' },
            'F' => { 'code3' => 'Phe', 'name' => 'Phenylalanine' },
            'G' => { 'code3' => 'Gly', 'name' => 'Glycine' },
            'H' => { 'code3' => 'His', 'name' => 'Histidine' },
            'I' => { 'code3' => 'Ile', 'name' => 'Isoleucine' },
            'K' => { 'code3' => 'Lys', 'name' => 'Lysine' },
            'L' => { 'code3' => 'Leu', 'name' => 'Leucine' },
            'M' => { 'code3' => 'Met', 'name' => 'Methionine' },
            'N' => { 'code3' => 'Asn', 'name' => 'Asparagine' },
            'P' => { 'code3' => 'Pro', 'name' => 'Proline' },
            'Q' => { 'code3' => 'Gln', 'name' => 'Glutamine' },
            'R' => { 'code3' => 'Arg', 'name' => 'Arginine' },
            'S' => { 'code3' => 'Ser', 'name' => 'Serine' },
            'T' => { 'code3' => 'Thr', 'name' => 'Threonine' },
            'V' => { 'code3' => 'Val', 'name' => 'Valine' },
            'W' => { 'code3' => 'Trp', 'name' => 'Tryptophan' },
            'Y' => { 'code3' => 'Tyr', 'name' => 'Tyrosine' }
        );
        my ($class, $protein1) = @_; my $protein3 = '';
        if (is_valid_protein($class, $protein1)) {
            for (my $i = 0; $i < length($protein1); $i++) {
                my $aa = substr($protein1, $i, 1);
                $protein3 .= $amino_acids{$aa}{code3};
            }
        }
        return $protein3;
    }

The client sample client5.pl is a command line program, that asks the user for a protein sequence (supposed to be 1-letter codes), and that uses our SOAP service to translate the sequence to 3-letter codes. Here is the source:

    use strict; use warnings;
    use SOAP::Lite;
    print "Protein sequence (1-letter codes)?\n";
    my $protein = <STDIN>; chomp($protein);
    my $soap = SOAP::Lite
        -> uri("http://192.168.40.1/Proteins")
        -> proxy("http://192.168.40.1/cgi-bin/server5.pl")
        -> protein_code3($protein);
    $protein = $soap->result;
    if ($protein) {
        print "Protein sequence (3-letter codes):";
        my $c = 0;
        for (my $i = 0; $i < length($protein); $i+=3) {
            if ($c == 0) {
                print "\n";
            }
            $c++;
            if ($c == 20) {
                $c = 0;
            }
            print substr($protein, $i, 3);
            print " ";
        }
    }
    else {
        print "This is not a valid protein sequence!";
    }
    print "\n";

With a server implemented as CGI script, the server-endpoint is not a server, but this script. Thus the argument to pass to the proxy() method has to be the URL of the CGI script (on the Apache webserver).

The screenshot shows the client output.

SOAP client with Perl - Command line client accessing a CGI server

Example 6: Autodispatching.

SOAP::Lite provides an autodispatching feature, that uses UNIVERSAL::AUTOLOAD to catch all unknown method calls. Using this feature makes it possible to call remote functions with the same syntax as local ones. Just be aware that all calls to undefined methods will result in an attempt to use SOAP.

The client sample client2b.pl is a re-write of the client client2.pl using autodispatching. Here is the code (the script output is of course the same as the one seen for client2.pl).

    use strict; use warnings;
    use SOAP::Lite +autodispatch =>
        uri => "http://wk-win10:4080/Hello",
        proxy => "http://wk-win10:4080/";
    print Hello->hello_user(), "\n";
    print Hello->hello_user("Aly"), "\n";

The client sample client4b.pl is a re-write of the client client4.pl using autodispatching. Here is the code (the script output is of course the same as the one seen for client4.pl).

    #!C:/Programs/Strawberry/win64/perl/bin/perl.exe
    use strict; use warnings;
    use SOAP::Lite +autodispatch =>
    uri => "http://192.168.40.1:4080/MYDATETIME",
    proxy => "http://192.168.40.1:4080/";
    print "Content-Type: text/html\n\n";
    print "<html><body>";
    print "<p>";
    print "Actual date: ", MYDATETIME->day_of_week(), ", ", MYDATETIME->get_date_long(), "<br/>";
    print "Actual time: ", MYDATETIME->get_time();
    print "</p>";
    print "</body></html>";

When calling the remote methods, don't forget that we are in an OO programming environment. This means that the functions actually are methods, and you'll have to specify a class when calling them!

Example 7: Dynamic service dispatching.

"When a SOAP request is received by a server, it gets bound to the class specified in the request. The class could be already loaded on server side (on server startup, or as a result of previous calls), or might be loaded on demand, according to server configuration. Dispatching is the process of determining of which class should handle a given request, and loading that class, if necessary. Static dispatch means that name of the class is specified in configuration, whereas dynamic means that only a pool of classes is specified, in, say, a particular directory, and that any class from this directory can be accessed.", Paul Kulchenko writes in his article Quick Start with SOAP at perl.com.

This means that we can put the service logic into some Perl modules, that we save in the directory, the path of which we specify as argument of the dispatch() method. This allows to add further services, based on further classes, without the need to review the source code of the server script.

The server sample server6.pl provides 2 services. The first one for counting the bases of a DNA sequence, the second one for translating a 1-letter-code protein sequence to 3-letter-code. For this purpose, let's write two Perl modules, called DNA.pm and Proteins.pm, and place them into "some local directory" (I use C:\Users\allu\Programming\Perl\SOAP).

Here is the code of DNA.pm:

    use strict; use warnings;
    package DNA;
    sub dna_count {
        my ($class, $dna) = @_;
        my ($len, $a, $c, $g, $t) = (0, 0, 0, 0, 0);
        if ($dna) {
            $len = length($dna); my $dna0;
            $dna0 = $dna; $dna0 =~ s/[^a]//gi; $a = length($dna0);
            $dna0 = $dna; $dna0 =~ s/[^c]//gi; $c = length($dna0);
            $dna0 = $dna; $dna0 =~ s/[^g]//gi; $g = length($dna0);
            $dna0 = $dna; $dna0 =~ s/[^t]//gi; $t = length($dna0);
        }
        return ($len, $a, $c, $g, $t);
    }
    1;

And the code of Proteins.pm:

    use strict; use warnings;
    package Proteins;
    sub is_valid_protein {
        my ($class, $protein) = @_; my $valid = 0;
        my $aa = 'ACDEFGHIKLMNPQRSTVWY';
        if ($protein =~ /^([$aa]+)$/) {
            $valid = 1;
        }
        return $valid;
    }
    sub protein_code3 {
        my %amino_acids = (
            'A' => { 'code3' => 'Ala', 'name' => 'Alanine' },
            'C' => { 'code3' => 'Cys', 'name' => 'Cysteine' },
            .....
            'W' => { 'code3' => 'Trp', 'name' => 'Tryptophan' },
            'Y' => { 'code3' => 'Tyr', 'name' => 'Tyrosine' }
        );
        my ($class, $protein1) = @_; my $protein3 = '';
        if (is_valid_protein($class, $protein1)) {
            for (my $i = 0; $i < length($protein1); $i++) {
                my $aa = substr($protein1, $i, 1);
                $protein3 .= $amino_acids{$aa}{code3};
            }
        }
        return $protein3;
    }
    1;

I implemented the server sample server6.pl, that should provide the "DNA" and "Proteins" related services, as a stand-alone HTTP server listening on port 4080. Here is the source:

    use strict; use warnings;
    use SOAP::Transport::HTTP;
    my $server = SOAP::Transport::HTTP::Daemon
        -> new (LocalPort => 4080,
                    LocalAddr => '192.168.40.1')
        -> dispatch_to('C:/Users/allu/Programming/Perl/SOAP');
    print "SOAP server URL = ", $server->url, "\n";
    $server->handle;

Let's consider two command line clients client63.pl and client65.pl, the first one for DNA base counting, the second one for protein translation.

The code of client63.pl is exactly the same as for client3.pl.

    use strict; use warnings;
    use SOAP::Lite;
    my @bases = ('adenine', 'cytosine', 'guanine', 'thymine');
    print "Enter DNA sequence? ";
    my $dna = <STDIN>; chomp($dna); print "\n";
    my $soap = SOAP::Lite
        -> uri("http://192.168.40.1:4080/DNA")
        -> proxy("http://192.168.40.1:4080/")
        -> dna_count($dna);
    my $len = $soap->result;
    my @counts = $soap->paramsout;
    print "Sequence length = $len\n";
    print "Sequence bases:\n";
    for (my $i = 0; $i < scalar @bases; $i++) {
        printf('%-10s', "  $bases[$i]");
        printf('%4u', $counts[$i]);
        print "\n";
    }
    print "\n";

The code of client65.pl is the same as for client5.pl, with the adaption to use the stand-alone server at port 4080, instead of the CGI-based server of example 5. Here is the source.

    use strict; use warnings;
    use SOAP::Lite;
    print "Protein sequence (1-letter codes)?\n";
    my $protein = <STDIN>; chomp($protein);
    my $soap = SOAP::Lite
        -> uri("http://192.168.40.1:4080/Proteins")
        -> proxy("http://192.168.40.1:4080")
        -> protein_code3($protein);
    $protein = $soap->result;
    if ($protein) {
        print "Protein sequence (3-letter codes):";
        my $c = 0;
        for (my $i = 0; $i < length($protein); $i+=3) {
            if ($c == 0) {
                print "\n";
            }
            $c++;
            if ($c == 20) {
                $c = 0;
            }
            print substr($protein, $i, 3);
            print " ";
        }
    }
    else {
        print "This is not a valid protein sequence!";
    }
    print "\n";

The screenshot shows the execution of client63.pl, followed by the execution of client65.pl.

SOAP client with Perl - Two clients accessing a dynamically dispatching server

Use the following link to download the source code of all tutorial samples.

This terminates my introduction concerning the creation of simple SOAP client-server applications using Perl. You can find some further information (in particular, concerning error handling) in the mentioned article at perl.com. If you are serious about SOAP, the O'Reilly book Programming Web Services with Perl (available as PDF on the Internet) should be helpful.


If you find this text helpful, please, support me and this website by signing my guestbook.