Skip to content

About

Python bindings for zmux: a QMux-compatible stream multiplexer with multipath, lanes, 0-RTT and session resumption over TCP, TLS, QUIC, WebSocket and HTTP.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

zmuxio

CI PyPI Python versions License

Python bindings for zmux: multiplexed streams and datagrams over one or many reliable byte pipes, wire-compatible with QMux (draft-ietf-quic-qmux). With a plain QMux peer a session is a single-pipe QMux session; between two zmux endpoints it adds multipath, resumption, stream preambles, priorities and datagram flows.

The protocol and all I/O run in the Rust implementation on a runtime managed by the extension; Python awaits the results on its asyncio loop.

pip install zmuxio      # import zmux; Python 3.12+, also free-threaded 3.14t

Prebuilt wheels: Linux (manylinux) x86_64, aarch64, armv7, i686, ppc64le, s390x and riscv64; Linux (musllinux) x86_64, aarch64, armv7 and i686; macOS x86_64 and arm64; Windows x86_64 and x86. Other platforms (Windows on arm64, for example) install from the sdist, which needs a Rust toolchain.

import asyncio, zmux

async def main():
    async with zmux.Listener() as listener:
        addr = await listener.serve_tcp("127.0.0.1:0")

        async def serve():
            async for session in listener:
                async for send, recv in session:
                    await send.write(await recv.read_to_end())
                    await send.finish()

        server = asyncio.create_task(serve())
        async with await zmux.connect_tcp(addr) as session:
            send, recv = await session.open_bi(info=b"echo")
            await send.write(b"hello")
            await send.finish()
            print(await recv.read_to_end())
        server.cancel()

asyncio.run(main())

Carriers: TCP, TLS (TlsConfig from PEM paths or bytes; clients default to the bundled web roots), QUIC (with lanes), WebSocket, HTTP/1.1, HTTP/2 (Http2Connection opens several pipes on one connection), HTTP/3 and WebTransport. dial_x(target, *, ...) gives a PipeIo, connect_x(target, *, tls=None, config=None, ...) a Session, and Listener.serve_x(addr, *, ...) accepts. pipe_from_streams(reader, writer) runs a session over any asyncio stream pair (slower: bytes pass through Python).

session = await zmux.connect_quic("example.net:443", config=zmux.Config(idle_timeout=30))
async with session:
    send, recv = await session.open_bi(info=b"route", priority=zmux.Priority(1))
    async with send:                      # finish on exit, reset on error
        await send.write(b"request")
    async for chunk in recv:              # until the end of the stream
        ...

Streams: finish() ends the sending half, reset(code) abandons it, recv.stop(code) asks the peer to stop sending. Every coroutine is cancellation-safe; use asyncio.timeout() for deadlines (durations are seconds). open_connection(session) returns an asyncio-style (reader, StreamWriter) pair whose drain() applies backpressure.

Events and statistics:

async for event in session.events():
    match event:
        case zmux.PipeStateChanged(pipe=p, state=zmux.PipeState.FAILED, failure=why):
            log.warning("pipe %d failed: %s", p, why)
        case zmux.Suspended() | zmux.Resumed():
            log.info("%s", event.kind)
print(session.stats().bytes_sent, [s.smoothed_rtt for s in session.pipe_stats()])

Multipath: give both ends Config(psk=...) (or tls_auth=True over TLS/QUIC), then session.add_pipe(await zmux.dial_tcp(addr), policy=zmux.PipePolicy(priority=1)). For an out-of-band join, r = session.reserve_pipe(), send r.header as the Zmux-Pipe-Join request header (dial_websocket(url, headers={"Zmux-Pipe-Join": r.header})), then r.attach(pipe). Resumption: Config(resume_timeout=..., dead_timeout=...), and session.revalidate_pipes() after a network change.

Early data: Config(early_data=True) toward a zmux peer sends the first streams before the peer's parameters arrive. QUIC 0-RTT: keep one QuicClient(tls, zero_rtt=True) with Config(carrier_early_data=True) at both ends and serve_quic(..., zero_rtt=True); session.zero_rtt reports what happened. Early data can be replayed, so servers should refuse non-idempotent requests on replayable streams:

async for send, recv in session:
    if recv.is_replayable and not idempotent(recv.preamble.info):
        await recv.stop(1)
        await send.reset(1)
        continue
    ...

On EarlyDataRejected (retryable is true) open the streams again on a new session without early data, and QuicClient.forget(server_name).

Errors derive from zmux.ZmuxError with kind (ErrorKind), retryable and code; SessionClosed adds reason and remote.

License: Apache-2.0.

About

Python bindings for zmux: a QMux-compatible stream multiplexer with multipath, lanes, 0-RTT and session resumption over TCP, TLS, QUIC, WebSocket and HTTP.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages