Developing A Game Engine with Perl: Part 7 - Fork
Pssssst... I DO NOT KNOW WHAT I AM DOING.
If you want to start reading from the beginning. Check out the first article in this series
Continuing from our last post, I talked about how ANSI Game Engine is a colourful telnet server. We left off with needing to fork the engines telnet server.
Player 2 has joined the game!
Time to level up our telnet server and make it multi-player with some knify forky.
I've added in the strftime
identifier from Perl's POSIX module to help with time stamping the output. The setsid
identifier is for starting a new session and group ID for each forked process. A.K.A, the child process. :sys_wait_h
is for returning without wait after the child process has exited, using the WNOHANG flag when calling waitpid(). This provides non-blocking wait for all pending zombie children.
Zombie Attack!!!
You see, when a process dies (exits), it becomes a zombie and needs to be reaped. This will be done when our parent process calls waitpid after receiving a CHLD signal, indicating the child has stopped or terminated.
Ok, I hope that will give you enough information to work with while dissecting the code:
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::INET;
use POSIX qw(setsid);
use POSIX qw(strftime);
use POSIX ":sys_wait_h";
sub timestamp {
my $epoc_seconds = time();
my $time = strftime "%H:%M:%S", localtime($epoc_seconds);
my $date = strftime "%m/%d/%Y", localtime;
my $return = $date . " " . $time;
return ($return);
}
sub logmsg { print timestamp . " -> $0 -> PID:$$: @_ \n" }
logmsg "Begin";
my $socket = new IO::Socket::INET (
LocalHost => '192.168.1.15',
LocalPort => '27777',
Proto => 'tcp',
Listen => SOMAXCONN,
ReuseAddr => 1
);
my $waitedpid = 0;
my $player_data;
my $player_socket;
sub REAPER {
local $!; # don't let waitpid() overwrite current error
logmsg "Ending Player's Game";
while ((my $pid = waitpid(-1, WNOHANG)) > 0 && WIFEXITED($?)) {
logmsg "Closed Game ID:$pid : WaitPid:$waitedpid : " . ($? ? " with exit $?" : "");
}
$SIG{CHLD} = \&REAPER; # loathe SysV
}
#if we get the CHLD signal call REAPER sub
$SIG{CHLD} = \&REAPER;
logmsg "Ready and waiting for connection";
while(1)
{
next unless $player_socket = $socket->accept();
logmsg ("Incomming Connection");
logmsg ("Spawning Player A Game");
my $pid = fork();
next if $pid; #NEXT if $pid exists (parent)
#As Child
setsid();
my $proc = $$;
logmsg ("Game ID:$proc -> Ready");
# get information about a newly connected player
my $player_address = $player_socket->peerhost();
my $player_port = $player_socket->peerport();
logmsg "Game ID:$proc -> Connection from $player_address:$player_port";
my $response = "Welcome Player: $player_address:$player_port. Press any key to disconnect.";
$player_socket->send($response);
while ($player_socket->connected()) {
$player_socket->recv($player_data, 1024);
if ($player_data) {
logmsg "Player Disconnecting $player_address : $player_port";
$socket->close();
logmsg "Player Disconnected";
last;
}
}
last;
}
exit;
Running this code and connecting with two players via SyncTERM, our client of choice, shows the following:
localhost:~/ANSIGameEngine # perl forking_telnet_server.pl
12/03/2021 18:16:58 -> forking_telnet_server.pl -> PID:15978: Begin
12/03/2021 18:16:58 -> forking_telnet_server.pl -> PID:15978: Ready and waiting for connection
12/03/2021 18:17:04 -> forking_telnet_server.pl -> PID:15978: Incomming Connection
12/03/2021 18:17:04 -> forking_telnet_server.pl -> PID:15978: Spawning Player A Game
12/03/2021 18:17:04 -> forking_telnet_server.pl -> PID:15979: Game ID:15979 -> Ready
12/03/2021 18:17:04 -> forking_telnet_server.pl -> PID:15979: Game ID:15979 -> Connection from 192.168.1.9:33422
12/03/2021 18:17:08 -> forking_telnet_server.pl -> PID:15978: Incomming Connection
12/03/2021 18:17:08 -> forking_telnet_server.pl -> PID:15978: Spawning Player A Game
12/03/2021 18:17:08 -> forking_telnet_server.pl -> PID:15980: Game ID:15980 -> Ready
12/03/2021 18:17:08 -> forking_telnet_server.pl -> PID:15980: Game ID:15980 -> Connection from 192.168.1.9:33428
12/03/2021 18:17:11 -> forking_telnet_server.pl -> PID:15979: Player Disconnecting 192.168.1.9 : 33422
12/03/2021 18:17:11 -> forking_telnet_server.pl -> PID:15979: Player Disconnected
12/03/2021 18:17:11 -> forking_telnet_server.pl -> PID:15978: Ending Player's Game
12/03/2021 18:17:11 -> forking_telnet_server.pl -> PID:15978: Closed Game ID:15979 : WaitPid:0 :
12/03/2021 18:17:13 -> forking_telnet_server.pl -> PID:15980: Player Disconnecting 192.168.1.9 : 33428
12/03/2021 18:17:13 -> forking_telnet_server.pl -> PID:15980: Player Disconnected
12/03/2021 18:17:13 -> forking_telnet_server.pl -> PID:15978: Ending Player's Game
12/03/2021 18:17:13 -> forking_telnet_server.pl -> PID:15978: Closed Game ID:15980 : WaitPid:0 :
How it all works
The main (parent) process that accepts new incoming telnet requests is PID:15978
in the above example. After it sets up the listen server, it waits for a
connection request and creates a forked process when a new player
connects (child). The code distinguishes the parent (main waiting telnet
server) process from the child (player) process with the value fork()
returns. The parent process receives the child's (player) PID as the return value of fork()
, so it loops back up and waits for another player to connect. The child (player) process receives a value of 0
from fork()
, so we continue downward in the code. In Perl doing if($pid)
does NOT evaluate TRUE
if $pid == (0 || undef)
, which is what the child (player) process will receive as the returned value from fork()
. We give the child (player) process a new session, record it's PID ($$
)
and wait for them to press any key. When the player presses a key the
socket is closed and the child (player) process exists and becomes a
zombie. This is when the parent (main) process receives the CHLD signal ($SIG{CHLD}
) and calls REAPER
How about you?
Have you worked with fork before? Have you unleashed a zombie apocalypse forgetting to reap? Comment about your experience, I'd love to hear your stories.
If you have any suggestions or comments please share constructively. Also please visit our social media pages for lots of fun videos and pictures showing the game engine in action.
ANSI Game Engine on Instagram
ANSI Game Engine on Facebook
Prev << Part 6 - A Colourful Telnet Server
Next >> Coming Soon
Cheers!
Shawn
Leave a comment