Skip to content

Networking and Daemons

System Programming with Perl

Version: 1.1 Year: 2026


Copyright (c) 2025-2026 Ryan Thomas Robson / Robworks Software LLC. Licensed under CC BY-NC-ND 4.0. You may share this material for non-commercial purposes with attribution, but you may not distribute modified versions.


Perl grew up on networked Unix systems. Sockets, forking, and signal handling map directly to system calls that Perl has wrapped since version 1. This guide covers TCP and UDP clients and servers, HTTP requests, JSON encoding, daemon processes, and event-driven frameworks for concurrent I/O.


TCP Sockets with IO::Socket::INET

IO::Socket::INET provides an object-oriented interface over the raw socket/bind/listen/accept system calls. It ships with core Perl.

TCP Server

A basic TCP server binds to a port, listens for connections, and handles each client:

use IO::Socket::INET;

my $server = IO::Socket::INET->new(
    LocalPort => 9000,
    Proto     => 'tcp',
    Listen    => 5,
    ReuseAddr => 1,
) or die "Cannot create server: $!\n";

while (my $client = $server->accept()) {
    while (my $line = <$client>) {
        chomp $line;
        print $client "Echo: $line\n";
    }
    close $client;
}

Listen sets the backlog queue size. ReuseAddr lets you restart the server immediately without waiting for the kernel to release the port.

Blocking Accept

This server handles one client at a time. While serving client A, client B waits in the backlog queue. For concurrent clients, you need fork, threads, or an event loop - covered later in this guide.

TCP Client

use IO::Socket::INET;

my $sock = IO::Socket::INET->new(
    PeerHost => 'localhost',
    PeerPort => 9000,
    Proto    => 'tcp',
) or die "Cannot connect: $!\n";

print $sock "Hello, server!\n";
my $reply = <$sock>;
print "Server says: $reply";
close $sock;

The socket object is a filehandle. You read from it with <$sock> and write with print $sock - the same I/O model as regular files.

TCP Client-Server Flow

sequenceDiagram
    participant C as Client
    participant S as Server
    S->>S: bind() + listen()
    C->>S: connect()
    S->>S: accept()
    S-->>C: Connection established
    C->>S: send("Hello")
    S->>S: Process data
    S-->>C: send("Echo: Hello")
    C->>S: send("quit")
    S-->>C: close()
    C->>C: close()

UDP Sockets

UDP is connectionless - no handshake, no guaranteed delivery, no ordering. Each send is an independent datagram. This makes UDP appropriate for DNS queries, logging, metrics, and real-time data where dropped packets are acceptable.

A UDP server binds to a port and calls recv in a loop. A UDP client specifies the peer at creation time and sends immediately - no handshake:

# Server
my $srv = IO::Socket::INET->new(LocalPort => 9001, Proto => 'udp')
    or die "Cannot bind: $!\n";
while (1) {
    $srv->recv(my $data, 1024);
    chomp $data;
    $srv->send("ACK: $data\n");
}

# Client
my $cli = IO::Socket::INET->new(
    PeerHost => 'localhost', PeerPort => 9001, Proto => 'udp',
) or die "Cannot create socket: $!\n";
$cli->send("ping\n");
$cli->recv(my $reply, 1024);
print "Got: $reply";

When to Use UDP

Choose UDP for fire-and-forget scenarios: syslog forwarding, StatsD metrics, DNS lookups, or any situation where retransmission logic lives in your application layer. For everything else, use TCP.


HTTP Clients

HTTP::Tiny

HTTP::Tiny ships with Perl since version 5.14. No CPAN install needed:

use HTTP::Tiny;
my $http = HTTP::Tiny->new(timeout => 10);
my $res  = $http->get('https://httpbin.org/get');

if ($res->{success}) {
    print $res->{content};
} else {
    warn "Failed: $res->{status} $res->{reason}\n";
}

The response is a hash reference with keys success, status, reason, content, and headers. POST requests pass a content body and headers:

my $res = $http->post('https://httpbin.org/post', {
    content => '{"key": "value"}',
    headers => { 'Content-Type' => 'application/json' },
});

HTTP::Tiny handles redirects, connection keep-alive, and HTTPS if IO::Socket::SSL is installed.

LWP::UserAgent

LWP::UserAgent is the full-featured HTTP client on CPAN. It supports cookies, authentication, proxies, file uploads, and content negotiation:

use LWP::UserAgent;

my $ua = LWP::UserAgent->new(timeout => 10);
my $res = $ua->get('https://api.github.com/zen');

if ($res->is_success) {
    print $res->decoded_content, "\n";
} else {
    warn "Error: ", $res->status_line, "\n";
}

The response is an HTTP::Response object with methods like is_success, status_line, and decoded_content.

Feature HTTP::Tiny LWP::UserAgent
Core Perl Yes (5.14+) No (CPAN)
HTTPS Needs IO::Socket::SSL Needs LWP::Protocol::https
Cookies Manual Built-in
File upload Manual Built-in

Mojo::UserAgent

Mojo::UserAgent is part of the Mojolicious framework and supports async HTTP with promises:

use Mojo::UserAgent;
my $ua = Mojo::UserAgent->new;

# Synchronous
my $res = $ua->get('https://httpbin.org/get')->result;
print $res->json->{origin}, "\n";

# Asynchronous - two requests concurrently
my $p1 = $ua->get_p('https://httpbin.org/delay/1');
my $p2 = $ua->get_p('https://httpbin.org/delay/1');

Mojo::Promise->all($p1, $p2)->then(sub {
    print "Both requests complete\n";
})->catch(sub { warn "Failed: @_\n" })->wait;

Promise->all runs both requests concurrently - two 1-second requests finish in roughly 1 second instead of 2.


JSON Handling

JSON::MaybeXS auto-detects the fastest available JSON backend (Cpanel::JSON::XS, JSON::XS, or pure-Perl JSON::PP):

use JSON::MaybeXS qw(encode_json decode_json);

my $json = encode_json({ name => 'Perl', year => 1987 });
my $data = decode_json($json);
print $data->{name}, "\n";    # Perl

For pretty-printed output, use the OO interface with pretty => 1 and canonical => 1 (sorted keys).

JSON Boolean Values

JSON's true/false map to JSON::PP::Boolean objects (behave like 1/0). To create JSON booleans from Perl, use JSON::MaybeXS::true/JSON::MaybeXS::false, or \1/\0.


Writing Daemons

A daemon is a long-running background process with no controlling terminal. The classic Unix double-fork ensures the daemon cannot reacquire a controlling terminal:

use POSIX qw(setsid);

sub daemonize {
    my $pid = fork();
    die "First fork failed: $!\n" unless defined $pid;
    exit 0 if $pid;

    setsid() or die "setsid failed: $!\n";

    $pid = fork();
    die "Second fork failed: $!\n" unless defined $pid;
    exit 0 if $pid;

    chdir '/' or die "Cannot chdir to /: $!\n";
    umask 0;

    open STDIN,  '<', '/dev/null' or die "Cannot redirect STDIN: $!\n";
    open STDOUT, '>', '/dev/null' or die "Cannot redirect STDOUT: $!\n";
    open STDERR, '>', '/dev/null' or die "Cannot redirect STDERR: $!\n";
}

Daemon Lifecycle

flowchart TD
    A[Parent Process] -->|fork| B[Child 1]
    A -->|exit| C[Parent exits]
    B -->|setsid| D[New session leader]
    D -->|fork| E[Child 2 - The Daemon]
    D -->|exit| F[Session leader exits]
    E -->|chdir /| G[Change working directory]
    G -->|close stdin/stdout/stderr| H[Redirect to /dev/null]
    H -->|write PID file| I[Running daemon]
    I -->|signal| J{Signal received}
    J -->|SIGTERM| K[Cleanup and exit]
    J -->|SIGHUP| L[Reload configuration]
    J -->|SIGUSR1| M[Log rotation]

PID File Management

A PID file records the daemon's process ID so management scripts can send signals to it. Lock the file with flock to prevent duplicate instances:

use Fcntl ':flock';

sub write_pidfile {
    my ($path) = @_;
    open my $fh, '>', $path or die "Cannot open PID file $path: $!\n";
    flock($fh, LOCK_EX | LOCK_NB)
        or die "Another instance is running (PID file locked)\n";
    print $fh $$;
    return $fh;    # Keep open to hold the lock
}

By holding the filehandle open, no other process can acquire the lock until this one exits.


Proc::Daemon

The Proc::Daemon CPAN module wraps the double-fork pattern into a single method call:

use Proc::Daemon;

my $daemon = Proc::Daemon->new(
    work_dir     => '/tmp',
    pid_file     => '/tmp/myapp.pid',
    child_STDOUT => '/var/log/myapp.log',
    child_STDERR => '/var/log/myapp.err',
);

my $pid = $daemon->Init();
exit 0 if $pid;    # Parent exits

# Daemon code runs here
while (1) {
    do_work();
    sleep 10;
}

Proc::Daemon handles forking, session creation, directory change, filehandle redirection, and PID file writing. Init returns the child PID to the parent and 0 to the daemon.


Signal Handling in Long-Running Processes

Daemons need to respond to signals - the Unix mechanism for inter-process communication. Perl exposes signal handlers through the %SIG hash:

my $running = 1;
$SIG{TERM} = sub { warn "SIGTERM received\n"; $running = 0 };
$SIG{HUP}  = sub { warn "SIGHUP received\n";  reload_config() };
$SIG{INT}  = sub { $running = 0 };

while ($running) {
    do_work();
    sleep 1;
}
cleanup();
exit 0;

Common Signals

Signal Typical Daemon Use
SIGTERM Graceful shutdown
SIGINT Ctrl-C (interactive)
SIGHUP Reload configuration
SIGUSR1 Rotate logs / dump state
SIGCHLD Reap child processes
SIGPIPE Broken socket (ignore it)

SIGPIPE Kills Daemons

Writing to a closed socket sends SIGPIPE, which terminates the process by default. Every network daemon should ignore it: $SIG{PIPE} = 'IGNORE'. Then check the return value of print or syswrite instead.

Reaping Child Processes

Forking servers must reap children to prevent zombie processes. Set a SIGCHLD handler with non-blocking waitpid:

use POSIX ':sys_wait_h';
$SIG{CHLD} = sub {
    while ((my $pid = waitpid(-1, WNOHANG)) > 0) { }
};

Process Supervision with systemd

Production daemons should be managed by a process supervisor. systemd is the standard on modern Linux. A unit file describes how to manage your service:

[Unit]
Description=My Perl Application
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/perl /opt/myapp/server.pl
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
User=myapp
WorkingDirectory=/opt/myapp
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Save this as /etc/systemd/system/myapp.service, then manage it with systemctl:

sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp

Type=simple vs. Type=forking

With Type=simple, systemd expects your process to stay in the foreground - do not daemonize. systemd handles backgrounding. With Type=forking, systemd expects the process to fork and reads the PID file to track the child. Type=simple is preferred for new services.

When using Type=simple, your Perl script runs as a foreground process. Set $| = 1 to unbuffer STDOUT so log lines appear immediately in journald:

#!/usr/bin/perl
use strict;
use warnings;

$| = 1;
my $running = 1;
$SIG{TERM} = sub { $running = 0 };
$SIG{HUP}  = sub { reload_config() };
$SIG{PIPE} = 'IGNORE';

while ($running) {
    do_work();
    sleep 1;
}
exit 0;

Event-Driven Programming

Blocking I/O handles one connection at a time. Event-driven I/O multiplexes many connections in a single process using select, poll, or epoll under the hood.

AnyEvent

AnyEvent provides a unified API across multiple event loop backends (EV, Event, POE, or its own pure-Perl loop):

use AnyEvent;
use AnyEvent::Socket;
use AnyEvent::Handle;

my $cv = AnyEvent->condvar;

tcp_server undef, 9000, sub {
    my ($fh, $host, $port) = @_;
    my $handle = AnyEvent::Handle->new(
        fh       => $fh,
        on_error => sub { $_[0]->destroy },
        on_eof   => sub { $_[0]->destroy },
    );
    $handle->on_read(sub {
        $handle->push_read(line => sub {
            my (undef, $line) = @_;
            $handle->push_write("Echo: $line\n");
        });
    });
};

$cv->recv;    # Enter the event loop

The condition variable ($cv) is the event loop entry point. $cv->recv blocks until $cv->send is called. Each connection gets its own AnyEvent::Handle for async I/O.

IO::Async

IO::Async is another event-driven framework, structured around a central loop with notifier objects. Where AnyEvent uses bare callbacks, IO::Async wraps everything in objects. It has built-in Future support for composing async operations.

use IO::Async::Loop;
use IO::Async::Listener;

my $loop = IO::Async::Loop->new;

$loop->add(IO::Async::Listener->new(
    on_stream => sub {
        my (undef, $stream) = @_;
        $stream->configure(on_read => sub {
            my ($self, $buffref, $eof) = @_;
            while ($$buffref =~ s/^(.*)\n//) {
                $self->write("Echo: $1\n");
            }
            return 0;
        });
        $loop->add($stream);
    },
));

$loop->listen(
    addr => { family => 'inet', socktype => 'stream', port => 9000 },
)->get;
$loop->run;
Feature AnyEvent IO::Async
Style Callback-based Object notifiers
Timer AnyEvent->timer(...) $loop->delay_future(...)
Futures AnyEvent::Future Built-in Future
Ecosystem Large (AnyEvent::*) Growing (Net::Async::*)

Both frameworks achieve the same goal: handling thousands of concurrent connections in a single process without threads or forks. AnyEvent is callback-centric. IO::Async uses an object hierarchy. Choose whichever fits your mental model.


Further Reading


Previous: Text Processing and One-Liners | Next: Web Frameworks and APIs | Back to Index

Comments