Networking and Daemons¶
System Programming with Perl¶
Version: 1.1 Year: 2026
Copyright Notice¶
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:
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:
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¶
- perlipc - Perl interprocess communication: signals, pipes, sockets
- IO::Socket::INET documentation - TCP/UDP socket interface
- HTTP::Tiny documentation - lightweight HTTP client (core module)
- LWP::UserAgent documentation - full-featured HTTP client
- Mojo::UserAgent documentation - async HTTP with promises
- JSON::MaybeXS documentation - fast, portable JSON handling
- AnyEvent documentation - event-driven programming
- IO::Async documentation - asynchronous I/O framework
- systemd.service man page - service unit configuration
- Network Programming with Perl (Stein) - comprehensive reference
Previous: Text Processing and One-Liners | Next: Web Frameworks and APIs | Back to Index