blackbull.server.protocol_registry¶
blackbull.server.protocol_registry
¶
Protocol bindings own detection, framing reads and actor construction.
ConnectionActor peeks without consuming; bindings claim by ALPN or ordered cleartext detection. Port-bound raw protocols skip detection. Keep internal bindings out of the top-level public exports.
RawProtocolHandler = Callable[['AbstractReader', 'AbstractWriter', 'ProtocolContext'], Awaitable[None]]
module-attribute
¶
Async (reader, writer, ctx) -> None — owns one connection's lifetime.
ConnectionView
dataclass
¶
Per-connection inputs for the selected ProtocolBinding to construct its actor.
Http1Binding
¶
Http2Binding
¶
ProtocolBinding
¶
One connection-level protocol.
A binding declares how many leading bytes it needs to recognise a
connection (detect_prefix_len) and whether it claims a given
peeked prefix; the winner's single serve then performs its own
protocol reads from a reader still positioned at the first byte, because
detection peeks without consuming. Only a reader that cannot peek hands
back what it took, and there a
PrefixReader restores the stream.
Every protocol-specific read is the binding's, never the dispatcher's —
the 24-byte HTTP/2 preface and the HTTP/1.1 \r\n request line
alike — which is what keeps ConnectionActor protocol-agnostic.
claims(prefix, alpn)
¶
Claim a shared-port connection by prefix; defaults to matches_cleartext.
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.
prefix_possible(prefix)
¶
Return True if prefix could still grow into a prefix this binding claims.
Consulted by the selector while prefix is shorter than
detect_prefix_len. Default: always True — the selector keeps
growing the peek to the full length. A binding that can rule prefix
out cheaply overrides this and returns False, so the next binding
claims without a full peek (the HTTP/2 preface's fixed first line rules
a GET out on its first byte).
serve(conn)
async
¶
Drive one connection. conn.reader is positioned at the first
byte of the stream, so the binding reads whatever framing it needs —
detection peeked without consuming anything on its behalf.
ProtocolContext
dataclass
¶
Context passed to a non-ASGI protocol handler.
Carries connection metadata and the shared 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. First-byte sniffing on shared ports (MQTT + HTTP on one port, say), reached only when ALPN did not already name the protocol.
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).
detection_order
property
¶
Bindings consulted during cleartext detection, in priority order:
registered raw detectors first (shared-port protocols such as MQTT),
then the ordered cleartext chain (http2 preface, http1
fallback). Cached, and rebuilt only by register.
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, tls=False, stateful=True)
¶
Register a non-ASGI protocol handler. Raises on duplicate name.
A detector enables shared-port sniffing (see
ProtocolDetector). When several registered detectors could
match the same first bytes, dispatch picks the first registered
one — raw_bindings preserves insertion order. tls=True serves
the binding's own port through the server's TLS machinery.
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.
A stateful binding stops claiming once more than one worker serves the shared listener: a worker that never saw the earlier exchange would answer wrongly. Its dedicated port is where it stays reachable.
disable_shared_dispatch()
¶
Stop claiming shared-port connections. Called by the master.