Perl Multithreaded Server Example

  • Author:xnite
  • Date:2016/08/16

Often times when writing a server you will need to be able to handle multiple client connections at a time. To do this you will want to create a threaded server.

The first thing that you will need to do is open a listening socket and create a loop that will continue listening for new clients. In this example I use IO::Socket::INET and a simple while(1) loop. We also need to create an array for our threads as shown in my Perl threading tutorial.

sub Main
{
	# flush after every write
	$| = 1;

	my ( $socket, $client_socket );
	
	# Bind to listening address and port
	$socket = new IO::Socket::INET (
		LocalHost => '127.0.0.1',
		LocalPort => '1337',
		Proto => 'tcp',
		Listen => 5,
		Reuse => 1
	) or die "Could not open socket: ".$!."\n";

	print "SERVER Waiting for client connections...\n";

	my @clients = ();
	while(1)
	{
		# Waiting for new client connection.
		$client_socket = $socket->accept();
		# Push new client connection to it's own thread
		push ( @clients, threads->create( \&clientHandler, $client_socket ) );
		foreach ( @clients ) 
		{
			if( $_->is_joinable() ) {
	    		$_->join();
	    	}
	    }
	}
	$socket->close();
	return 1;
}

The code above will accept connections from new users, then throw the client connection into a thread where commands can be processed.

To process commands you will need to create the client handler function and a while loop to handle the buffer. Within the while loop commands can be processed by matching buffer against regular expression. You could create a regular expression to get command & arguments then use a case-switch, but I won’t cover this method.

sub clientHandler
{
	# Socket is passed to thread as first (and only) argument.
	my ($client_socket) = @_;

	# Create hash for user connection/session information and set initial connection information.
	my %user = ();
    $user{peer_address} = $client_socket->peerhost();
	$user{peer_port} = $client_socket->peerport();

	print "Accepted New Client Connection From:".$user{peer_address}.":".$user{peer_port}."\n";

	# Let client know that server is ready for commands.
	print $client_socket "> ";

	# Listen for commands while client socket remains open
	while( my $buffer = <$client_socket> )
	{
		# Accept the command `HELLO` from client with optional arguments
		if( $buffer =~ /^HELLO(\s|$)/i )
		{
			#Example reply
			print $client_socket "HELLO THERE!!\n";
			print $client_socket "Your IP:\t".$user{peer_address}."\n";
			print $client_socket "Your Port:\t".$user{peer_port}."\n";
		}

		# This will terminate the client connection to the server
		if( $buffer =~ /^QUIT(\s|$)/i )
		{
			# Print to client, and print to STDOUT then exit client connection & thread
			print $client_socket "GOODBYE\n";
			print "Client exit from ".$user{peer_address}.":".$user{peer_port}."\n";
			$client_socket->shutdown(2);
			threads->exit();
		}
		print $client_socket "> ";
	}
	print "Client exit from ".$user{peer_address}.":".$user{peer_port}."\n";

	# Client has exited so thread should exit too
	threads->exit();
}

In the example above you will notice the command

1
quit
 allows the client to exit the server cleanly. This allows the server to issue the
1
$client_socket->shutdown()
 method which is a clean way of exiting a socket. Then it calls
1
threads-exit()
 which is how you cleanly exit a thread. By exiting this way the server won’t be keeping open potentially stale connections. It is also wise to add some sort of PING-PONG between the client and server to check that connections are still alive and active.

$ perl PerlServer.pl
SERVER Waiting for client connections...
Accepted New Client Connection From:127.0.0.1:52562
Client exit from 127.0.0.1:52562
$ telnet localhost 1337
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
> HELLO
HELLO THERE!!
Your IP:        127.0.0.1
Your Port:      63600
> QUIT
GOODBYE
Connection closed by foreign host.

Here is the full code for the example server

#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::INET;
use threads;

sub Main
{
	# flush after every write
	$| = 1;

	my ( $socket, $client_socket );
	
	# Bind to listening address and port
	$socket = new IO::Socket::INET (
		LocalHost => '127.0.0.1',
		LocalPort => '1337',
		Proto => 'tcp',
		Listen => 5,
		Reuse => 1
	) or die "Could not open socket: ".$!."\n";

	print "SERVER Waiting for client connections...\n";

	my @clients = ();
	while(1)
	{
		# Waiting for new client connection.
		$client_socket = $socket->accept();
		# Push new client connection to it's own thread
		push ( @clients, threads->create( \&clientHandler, $client_socket ) );
		foreach ( @clients ) 
		{
			if( $_->is_joinable() ) {
	    		$_->join();
	    	}
	    }
	}
	$socket->close();
	return 1;
}

sub clientHandler
{
	# Socket is passed to thread as first (and only) argument.
	my ($client_socket) = @_;

	# Create hash for user connection/session information and set initial connection information.
	my %user = ();
    $user{peer_address} = $client_socket->peerhost();
	$user{peer_port} = $client_socket->peerport();

	print "Accepted New Client Connection From:".$user{peer_address}.":".$user{peer_port}."\n";

	# Let client know that server is ready for commands.
	print $client_socket "> ";

	# Listen for commands while client socket remains open
	while( my $buffer = <$client_socket> )
	{
		# Accept the command `HELLO` from client with optional arguments
		if( $buffer =~ /^HELLO(\s|$)/i )
		{
			#Example reply
			print $client_socket "HELLO THERE!!\n";
			print $client_socket "Your IP:\t".$user{peer_address}."\n";
			print $client_socket "Your Port:\t".$user{peer_port}."\n";
		}

		# This will terminate the client connection to the server
		if( $buffer =~ /^QUIT(\s|$)/i )
		{
			# Print to client, and print to STDOUT then exit client connection & thread
			print $client_socket "GOODBYE\n";
			print "Client exit from ".$user{peer_address}.":".$user{peer_port}."\n";
			$client_socket->shutdown(2);
			threads->exit();
		}
		print $client_socket "> ";
	}
	print "Client exit from ".$user{peer_address}.":".$user{peer_port}."\n";

	# Client has exited so thread should exit too
	threads->exit();
}

# Start the Main loop
Main();

I hope this helped, sorry about the lack of indentation in some of the code, for some reason WordPress is messing it up in my code blocks.

Robert Whitney
I'm a geek, gamer and breaker of things. I'm a programmer by day and an apache attack helicopter by night. Some would call me their spirit animal.
Opinions expressed here, even 💩 ones, are my own and do not represent those of my employer or associates.
Referral Links

Using my referral links is the best way to help me pay for my projects and development servers and get something out of it for yourself.

Copyright©2011 - 2018, Robert Whitney; All rights reserved.