Skip to content

Networking (TCP)

The bundled TcpTransport runs real choreographies over TCP so the same projection works in one process (simulator) or across processes/machines.

The transport protocol

Every Transport implements the same two async methods:

async def send(self, src, dst, value)   # send `value` from role src to role dst
async def recv(self, src, dst) -> value # receive the next value from src at dst

TcpTransport gives each role a listening server (its inbox) and a pool of outbound connections (its writers). Values are pickle-serialized and framed with a 4-byte length prefix.

import asyncio
from klorpy.sockets import TcpTransport
from klorpy.runtime import run_role

async def main():
    a = TcpTransport("A", ("127.0.0.1", 0))   # port 0 = pick an ephemeral port
    b = TcpTransport("B", ("127.0.0.1", 0))
    await a.start()
    await b.start()
    # wire peers up after both are bound
    a.peers["B"] = ("127.0.0.1", b.port)
    b.peers["A"] = ("127.0.0.1", a.port)
    try:
        ra, rb = await asyncio.gather(
            run_role(ping_pong, "A", a, {"x": 42}),
            run_role(ping_pong, "B", b, {"x": 42}),
        )
        assert rb == 42
    finally:
        await a.close()
        await b.close()

asyncio.run(main())

Why run_role and not play_role here

play_role wraps a single role in its own asyncio.run loop, which is fine for one node. A multi-role TCP run needs all servers in one event loop, so use the async run_role and asyncio.gather.

Two OS processes

examples_network.py plays the same ping_pong with A and B in separate operating-system processes, connected over loopback TCP:

$ python examples_network.py
A -> result: NOOP
B <- result: 42
demo OK: A sent 42 to B over TCP; B returned 42

Each process builds its own TcpTransport and plays one role. TcpTransport._writer_to performs a bounded connect-retry (5 s), so a node may start connecting before its peer's server has bound.

Caveats

Pickle is not safe against untrusted data

TcpTransport uses pickle for serialization. This transport is a prototype for trusted, same-author endpoints — do not expose it as a public-facing service.

  • A node connects out to each peer lazily and pools the writer; each role must run its own TcpTransport and be reachable at its advertised address.
  • Roles on different machines: use real host addresses instead of 127.0.0.1, and make sure the listening port is reachable.
  • play_role + ConfigTransport remain the generic way to plug in any transport (an in-process test transport, an asyncio.Queue bridge, …). TcpTransport implements Transport directly, so run_role accepts it without a config wrapper.