DEV Community

Cover image for Developing A Game Engine with Perl: Part 7 - Fork
Shawn Holland
Shawn Holland

Posted on • Updated on

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.

Image description

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!!!

Image description
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;
Enter fullscreen mode Exit fullscreen mode

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 :  
Enter fullscreen mode Exit fullscreen mode

Image description

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 >> Part 8 - Vim

Cheers!
Shawn

Discussion (0)