blackbull.server.protocol_registry¶
blackbull.server.protocol_registry
¶
Unified protocol registry — Sprint 50.
BlackBull dispatches every accepted connection through a single
:class:ProtocolRegistry. http1 and http2 are built-in bindings;
non-HTTP protocols (raw TCP, and later MQTT/Redis) register their own bindings
via :meth:BlackBull.raw_handler / :meth:BlackBull.register_protocol_handler.
A binding owns protocol selection, its own framing reads, and Actor
construction. ConnectionActor peeks only a tiny protocol-agnostic
discriminator prefix (decouple-connection-detection, Stage 2); the 24-byte
HTTP/2 preface read and the HTTP/1.1 request-line read live in
:class:Http2Binding / :class:Http1Binding, reached through the single
:meth:ProtocolBinding.serve entry point.
Two dispatch routes:
- Detection (the shared HTTP listener):
ConnectionActorpeeks the discriminator and asks each :class:ProtocolBindingvia :meth:claims— ALPN first, then the ordered cleartext chain (http2preface,http1fallback) — then replays the peeked bytes to the winner's :meth:serve. - Port-bound (raw protocols): a binding registered with
port=gets its own listening socket; connections there skip detection entirely.
Note
Do not export the internal classes from blackbull/__init__.py; the
public surface is :meth:BlackBull.raw_handler and
:meth:BlackBull.register_protocol_handler.
RawProtocolHandler = Callable[['AbstractReader', 'AbstractWriter', 'ProtocolContext'], Awaitable[None]]
module-attribute
¶
Async (reader, writer, ctx) -> None — owns one connection's lifetime.
ConnectionView
dataclass
¶
Everything a :class:ProtocolBinding needs to build its Actor.
Assembled once per connection by :class:ConnectionActor and handed to the
selected binding's serve_* method. Keeps the binding API narrow and
decouples bindings from ConnectionActor's internals.
Http1Binding
¶
Http2Binding
¶
ProtocolBinding
¶
One connection-level protocol.
A binding declares how many leading bytes it needs to recognise a
connection (:attr:detect_prefix_len) and whether it :meth:claims a given
peeked prefix; the winner's single :meth:serve then performs its own
protocol reads from a reader positioned at the start of the stream (the
peeked bytes are replayed via a :class:~blackbull.server.recipient.PrefixReader).
Collapsing the old serve_alpn / serve_cleartext / serve_raw trio
into one serve(conn) is what lets ConnectionActor stay
protocol-agnostic (decouple-connection-detection, Stage 2): the 24-byte
HTTP/2 preface read and the HTTP/1.1 \r\n request-line read now live in
the bindings, not in the dispatcher.
claims(prefix, alpn)
¶
Unified detection predicate: does this binding own a connection whose first bytes are prefix (with negotiated alpn)?
The single selection seam for cleartext + shared-port dispatch
(decouple-connection-detection, Stage 1). Default delegates to
:meth:matches_cleartext; :class:RawBinding overrides it to consult
its :class:ProtocolDetector. alpn is accepted so a future binding
can claim on the negotiated token, not just the wire prefix.
matches_cleartext(first_line)
¶
Return True if this binding owns a cleartext connection whose first line is first_line. Default: no match.
on_detect_timeout(conn)
async
¶
Called when the peer connected but sent no discriminator within the
detection deadline. Default: close silently (the caller closes the
transport) — appropriate for a protocol with no meaningful "you were too
slow" wire message. HTTP overrides this to emit a 408. Lets
ConnectionActor stay free of protocol-specific status strings
(decouple-connection-detection, Stage 3).
serve(conn)
async
¶
Drive one connection. conn.reader is positioned at the first
byte of the stream (detection peeks are replayed), so the binding reads
whatever framing it needs — no bytes are pre-consumed on its behalf.
ProtocolContext
dataclass
¶
Context passed to a non-ASGI protocol handler.
Carries connection metadata and the shared :class:EventAggregator
without exposing any ASGI concept (no scope / receive / send).
ProtocolDetector
¶
Bases: ABC
Inspects the first bytes of a connection to identify the protocol.
Stateless — one instance is shared across all connections. Used for
first-byte sniffing on shared ports (e.g. MQTT + HTTP on one port);
consulted by ConnectionActor._dispatch() after ALPN detection and
before the http1 cleartext fallback.
ProtocolRegistry
¶
Single source of truth for all connection-level protocols.
Pre-populated with the built-in HTTP bindings; one instance per app. The
cleartext-detection order is fixed: http2 (preface) before http1
(fallback) — http1 must be last because it matches any first line.
cleartext_bindings
property
¶
Ordered cleartext-detection chain (http2 then http1).
port_bindings
property
¶
Raw bindings that need their own listening socket, keyed by port.
__bool__()
¶
Truthy once a non-HTTP protocol is registered (HTTP is always present).
register(name, handler, *, detector=None, port=None)
¶
Register a non-ASGI protocol handler. Raises on duplicate name.
A detector enables shared-port sniffing (see
:class:ProtocolDetector). When several registered detectors could
match the same first bytes, dispatch picks the first registered
one — raw_bindings preserves insertion order.
RawBinding
¶
Bases: ProtocolBinding
A user-registered non-ASGI protocol, bound to its own listening port.
detect_prefix_len
property
¶
Bytes the detector needs to recognise the protocol. A detector may
declare prefix_len; otherwise one byte (the common first-byte sniff,
e.g. MQTT's 0x10) suffices. Port-bound bindings never detect → 0.
claims(prefix, alpn)
¶
A raw binding claims a shared-port connection when its detector recognises the first bytes. Port-bound bindings (no detector) never claim via detection — they own their own listening socket instead.