Perl Sockets Swimming in a Thread Pool

I’ve written a simple multithreaded TCP server that uses sockets and a thread pool to handle client requests. This server is packaged into a class (DcServer) that can be trivial to use. Here’s how you might use DcServer to build a server that accepts text from clients and returns the same text with the characters in reverse order:

use DcServer;
my $server= DcServer->new(processor_cb => \&reverse_text);
$server->start;

sub reverse_text {
    my $data= shift;
    join('', reverse(split //, $data));
}

Run server.pl like this: perl server.pl

The server defaults to 10 threads, so you could have clients connecting concurrently to have their text reversed. And you could probably serve a great many of these clients per second.

I’ve also created code to help me build clients. You can, for example, build a client for the above server like this:

use DcClient;
my $message= shift;
die "Usage: perl client.pl string\n" unless defined($message);
my $c= DcClient->new;
print "$message => ", $c->query($message), "\n";

Run client.pl like this: perl client.pl "hello"

The code for the server is short and bare-bones and illustrates how to pass a socket from one thread to another.

Yes, I suffer from the NIH (not-invented-here) syndrome. I have no excuse for having written this. Let’s leave it at that. Nevertheless, I hope the code is useful to someone out there, directly or as a simple example of how to set up thread pools and how to share sockets among threads.

Here’s a little reference for how to use the DcServer module, which depends on these modules only:
– threads
– threads::shared
– Thread::Queue
– IO::Socket
– Time::HiRes

Overview

DcServer is a class that encapsulates a TCP server that implements a thread pool to handle client requests. The class allows the instantiator to provide call back functions for processing client requests, performing background work, and performing tasks after the server has shut down. The instantiator can shut down the server from within two of these callback functions. For example, the instantiator can shut down the server when certain arbitrary conditions are met (regardless of whether clients are connecting or not) or when a client explicitly requests it.

DcServer Methods

new
This method instantiates a server. It takes the following parameters.

  • host An IP address or host name that resolves to an IP address. This is to tell the server where it will be running. Clients should specify the same host name in order to connect. This value defaults to ‘localhost’ if you don’t specify it.
  • port The port on which the server will listen for connections. This value defaults to 8191 if you don’t provide it.
  • thread_count The number of threads that you want the server to start for its thread pool. The server uses the thread pool for handling client connections. This value defaults to 10 if you don’t specify it.
  • eom_marker This is the sequence of characters that the server and the client use to tell each other that they’re done transmitting data. This value defaults to “\\n\\.\\n”, which means “new line, period, new line”.
  • main_yield The server allows the instantiator to specify a callback function that the server will call in a loop. After calling this function, the server sleeps for main_yield seconds before calling the function again.. The value of main_yield defaults to 5 seconds.
  • main_cb A reference to a function that the server calls over and over again, independently of accepting and processing client connections. The server calls the function specified via main_cb with two positional parameters, $counter and $fnstop. The first parameter, $counter, contains an integer with the number of times that the server has called the main_cb function. The second parameter, $fnstop, is a code reference that, when called as a function, causes the server to stop. You can call the function in $fnstop like this: $fnstop->()

    When you call the $fnstop function, the function returns immediately, but by then the call has already initiated a server shutdown.

  • done_cb A reference to a function that the server calls when it has completed its shutdown procedure.
  • processor_cb A reference to a function that the server calls to process the data that the client has sent. The function should accept a string (the client’s request) and it should return another string (the result of processing the client’s request). More specifically, the server calls the processor_cb function with the following positional parameters: $data, $ip, $tid, and $fnstop. There following list describes these parameters:
    • $data: The data that the client sent to the server. This amounts to the client’s request. It is up to the instantiator to interpret and process that request.
    • $ip: The IP address of the client.
    • $tid: The ID of the thread that is handling the request.
    • $fnstop: A reference to a function that you can call to stop the server. You can call this function like this: $fnstop->()

    The processor_cb function should return a string consisting of an answer to the client’s request. The processor_cb function should be able to call the $fnstop function and still return data to the client. But if you do that, the call to $fnstop should occur immediately before the function returns, not before the function processes the client’s data.

start
This method takes no parameters. It simply starts the server that you previously instantiated with the new method.

Acknowledgements

While writing this code, I ran into some serious trouble with passing sockets from one thread to another. I was able to resolve the issue thanks to

something about sockets and threads that BrowserUk posted back in 2006.

Thanks BrowserUk! (And thanks PerlMonks!) I am not worthy of your guidance.

Source Code

You can view or download the code for the

DcServer and DcClient modules here.

You’re welcome also to add the code to CPAN or to POD it or to attempt to convince me to do any or all of this. I know that I should, but I’m such a sloth that I’ve never contributed even a single module to CPAN. The only thing I ask is that you let me know if you manage to improve the code in any significant way.