NAME
Net::BCCN - Broadcast Channel Notifications protocol
SYNOPSIS
use Net::BCCN;
my $bccn = Net::BCCN->new(
NAME => "host1/worker/$$",
ADDR => '255.255.255.255',
BIND => '0.0.0.0',
PORT => 1122,
DD => 1, # enable per-message dedup
SS => 100, # track speed stats over last 100 messages
DEBUG => 0,
);
$bccn->open() or die "open failed: " . $bccn->error();
# publish
$bccn->notify( 'supply-channel',
'txnid=12345:avail=1234:rc=00' );
# listen on a channel (returns one message, or undef if none waiting)
while (1)
{
my $msg = $bccn->listen( 'work-channel', { TIMEOUT => 1 } );
next unless $msg;
process( $msg );
}
DESCRIPTION
Net::BCCN implements the BCCN wire protocol - a lightweight LISTEN/NOTIFY-style notification fabric over UDP broadcast on a single network segment. Inspired by PostgreSQL's NOTIFY/LISTEN, but extended across machines and across processes that don't share a database connection.
Each datagram has the form:
BCCN1[<len><:<algo>=<sum>>?]<src>:<seq>:<chan>|<payload>
<len> decimal byte count of the body after "]"
<algo> optional integrity algorithm name (e.g. "hmac")
<sum> optional integrity check value
<src> sender identifier ("from")
<seq> sender's per-process sequence counter
<chan> channel name ("to")
<payload> opaque application-defined bytes
The current implementation handles plain BCCN1 datagrams (no integrity check). HMAC and directed-channel addressing are part of the protocol design but not yet implemented in this module.
Semantics are fire-and-forget. No acks, no persistence, no replay. Suits notify-fabric use cases where occasional packet loss is acceptable.
CONSTRUCTOR
new( %options )
Creates a new BCCN endpoint. Does not open the socket - call open() to do that.
Options:
- NAME
-
The local instance's name string used in outgoing datagrams as source name. Defaults to "?" (the protocol's unknown-sender placeholder). A typical convention is
host/program/pid, e.g.host4/processor/12345. - ADDR
-
Broadcast destination address. Defaults to
255.255.255.255(limited broadcast - never crosses routers). For directed subnet broadcast, use the subnet's broadcast address such as10.0.0.255. - BIND
-
Local bind address. Defaults to
0.0.0.0(all interfaces). - PORT
-
UDP port to bind and send on. Required. Sender and listener must agree on the port number.
- DD
-
If true, enable per-message deduplication. The key is the concatenation of (sender IP, src, seq, chan); duplicates within this key set are silently dropped. Useful when the application sends each notification twice for loss reduction. Periodically call clear_dd_lookup() to expire old entries.
- SS
-
If a positive integer N, enable speed statistics tracking over the last N messages. Per-channel and aggregate rates are exposed via stats(). If unset, no speed tracking.
- DEBUG
-
True value or positive integer to enable debug output. Currently used for selective trace prints during recv.
METHODS
open()
Creates the UDP socket, sets SO_REUSEADDR, SO_BROADCAST, and non-blocking mode, and binds to BIND:PORT. Returns 1 on success, undef on failure (call error() for details).
notify( $chan, $payload )
Builds a BCCN1 datagram with the current NAME as src, the next sequence number, the given channel and payload, and broadcasts it. Returns 1 on success, undef on failure (call error()).
The channel name is opaque to the protocol; sender and receivers agree on its meaning. Any whitespace-free string is valid.
The payload is opaque bytes; format is application's choice. Keep the whole datagram under 1400 bytes to avoid IP fragmentation.
listen( $chan, \%opt )
Pulls all currently available datagrams off the socket into per-channel queues, then returns one queued message from the named channel or undef if none is available.
Note that even the sender (caller of notify()) will receive its own message if calls listen() on the same channel as notify(). BCCN is just a transport, execution logic is up to the implementation of the client.
Options:
- TIMEOUT
-
Seconds to wait for the first datagram if the channel queue is empty. 0 (default) means non-blocking. Once at least one datagram has been queued, the method drains everything else that's immediately readable without further waiting.
Returned messages are hashrefs:
{
FROM => <src>, # sender identifier from datagram
FROM_IP4 => <ip address>, # sender IP from recvfrom()
FROM_PORT => <udp port>, # sender port from recvfrom()
CHANNEL => <chan>,
MSG => <payload>,
RTIME => <unix time>, # receive time
}
Note: datagrams for other channels read off the socket in the same pass are queued internally; subsequent listen() calls on those channels will return them.
The idea for the TIMEOUT is to allow shortest possible sleep between processing and housekeeping (when no message available and timeout expired). So calling listen() with timeout will check for incoming messages, and if any, will read them all and return either requested channel message or undef if none. When no messages are available it will block until timeout expired or until any new message arrive (regardless the requested channel or not). On returning undef, housekeeping or other work can be done.
error()
Returns the last error message, or undef if the last operation succeeded. Cleared at the start of each operation that sets it.
stats()
Returns a hashref of runtime statistics:
{
QSC => <count>, # total queues/channels count
SEEN_QS => [ <chan>, ... ], # all queues/channels ever seen
ACTIVE_QS => [ <chan>, ... ], # queues/channels with queued messages
QMC => { <chan> => <count>, ... }, # per-channel queue length
TQMC => { '*' => <count>, # total messages received
<chan> => <count>, # per-queue/channel total count
... },
DDC => <count>, # dedup table size (if DD enabled)
DDO => <unix time>, # oldest dedup entry (unix time)
SS => { '*' => <rate>, # messages per second (if SS enabled)
<chan> => <rate>, # per-queue/channel speed
... },
}
clear_dd_lookup( $seconds )
Removes dedup entries older than $seconds. Call periodically when DD is enabled to prevent unbounded growth of the dedup table. Has no effect when DD is disabled. call without argument or 0 to clear the entire table.
PROTOCOL DETAILS
The wire format is a printable ASCII envelope plus an opaque payload. The envelope is fixed across protocol versions; only the body interpretation changes between BCCN1, a hypothetical BCCN2, etc. Anything an intermediary needs for routing, authentication, or framing lives in the envelope; everything else is body.
This module accepts the optional :<algo>=<sum> integrity slot in the envelope when parsing (the parser regex allows it) but does not currently verify or generate it.
Reserved characters:
Space, tab, CR, LF: not allowed in src or chan.
|: separates body header from payload; first occurrence marks the boundary.:: separates src/seq/chan in the body header; not allowed inside src or chan.[ ]: envelope brackets.!as first character of chan: reserved for all-form ("!") and directed-form ("!<target>") delivery. Not implemented in this module yet.?as exact value of src: reserved unknown-sender placeholder.
LIMITATIONS
This module implements a minimal BCCN1 baseline. Not yet supported:
HMAC or other integrity check (envelope syntax recognised but not verified).
!-prefix channel forms: all-broadcast and directed delivery.Sequence-number-based per-sender dedup (only IP+src+seq+chan tuple dedup via the DD option).
Multicast delivery. The current revision is broadcast-only on a single L2 segment. Multicast is considered for next releases.
EXAMPLE
A small publisher/subscriber pair:
# publisher.pl
use Net::BCCN;
my $b = Net::BCCN->new( NAME => "tx-source/$$",
PORT => 5400 );
$b->open() or die $b->error();
my $i = 0;
while (1)
{
$b->notify( 'demo/tick', "i=$i ts=" . time() );
$i++;
sleep 1;
}
# subscriber.pl
use Net::BCCN;
my $b = Net::BCCN->new( NAME => "tx-sink/$$",
PORT => 5400,
DD => 1 );
$b->open() or die $b->error();
while (1)
{
my $msg = $b->listen( 'demo/tick', { TIMEOUT => 1 } );
next unless $msg;
print "$msg->{FROM_IP4} $msg->{FROM} -> $msg->{MSG}\n";
}
Run multiple subscribers on the same host - all of them will receive every datagram because the underlying socket has SO_REUSEADDR set.
SEE ALSO
PostgreSQL LISTEN/NOTIFY - the inspiration for BCCN's semantics.
IO::Socket::INET, IO::Select - the Perl networking primitives used internally.
AUTHOR
Vladi Belperchinov-Shabanski "Cade" <cade@noxrun.com>
<http://cade.noxrun.com>
https://github.com/cade-vs/bccn
COPYRIGHT AND LICENSE
Copyright (c) 2026 Vladi Belperchinov-Shabanski.
This module is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2 or, at your option, any later version.