API transports
APIServer consumes an ordered reliable byte stream. Every transport carries the same API frames: a four-byte big-endian length followed by that many JSON envelope bytes. The length marks a message; the transport supplies delivery and ordering.
The configurations below adapt the complete local client example.
Keep its auth-db, management-api, tokens and core.json. Node fragments here
are additions or replacements inside that example's nodes array, not separate
complete configurations. Stop the demo cleanly before editing and restart after
each transport change.
Direct TCP
The initial example already uses:
Python client -> TcpListener -> APIServer
The listener binds 127.0.0.1:9080 and its next is management-api. Run the
unchanged client command from the first example.
TCP reads may split prefixes and bodies or combine multiple frames; a client
must assemble the exact prefix and body lengths.
HTTP/1.1 body tunnel
Change api-listener.next to http-body, then add this node:
{
"name": "http-body",
"type": "HttpServer",
"settings": {
"http-version": "1.1",
"http1-mode": "single",
"upgrade": false,
"path": "/management",
"method": "POST"
},
"next": "management-api"
}
The chain becomes:
HTTP caller -> TcpListener -> HttpServer -> APIServer
Place complete framed API bytes in the HTTP request body, using Content-Length
or valid HTTP chunked encoding. HttpServer validates its configured method and
path, then forwards body bytes. API authentication remains inside each JSON
envelope; HTTP headers and URLs do not become API selectors or operation bodies.
This is a body tunnel, not REST routing or per-operation HTTP status codes.
The response body is chunked in this HTTP/1.1 setup. Dechunk it before reading the API length. One HTTP chunk is not one API frame. Read the four-byte API prefix and exactly its declared body bytes; do not wait for HTTP EOF or a final zero-length chunk. Single mode keeps the tunnel alive after the request body ends.
The generic api_client.py and administration CLI speak direct TCP only and do not
construct HTTP requests. For a single status request over this HTTP configuration, the standard
library's http.client handles the HTTP headers and dechunking:
import http.client
import json
from pathlib import Path
import struct
def exact(body, length):
data = bytearray()
while len(data) < length:
part = body.read(length - len(data))
if not part:
raise RuntimeError("HTTP body ended before the API frame was complete")
data.extend(part)
return bytes(data)
request_id = "http-status-1"
envelope = {
"id": request_id,
"auth": {"client": "monitor", "token": Path("monitor-token").read_text(encoding="ascii").strip()},
"selector": {"config": "central", "node": "auth-db"},
"operation": "status", "content_type": "application/json",
"body_encoding": "utf8", "body": "{}"
}
payload = json.dumps(envelope, separators=(",", ":")).encode("utf-8")
frame = struct.pack("!I", len(payload)) + payload
connection = http.client.HTTPConnection("127.0.0.1", 9080, timeout=10)
try:
connection.request("POST", "/management", body=frame,
headers={"Content-Type": "application/octet-stream"})
response = connection.getresponse()
if response.status != 200:
raise RuntimeError("HTTP tunnel request was rejected")
length = struct.unpack("!I", exact(response, 4))[0]
if not 0 < length <= 1024 * 1024:
raise RuntimeError("API response exceeded the client limit")
reply = json.loads(exact(response, length).decode("utf-8"))
if reply["id"] != request_id or reply["error"] is not None:
raise RuntimeError("API correlation or gateway error")
result = reply["result"]
if result["body_encoding"] != "utf8":
raise RuntimeError("Unexpected status response encoding")
print(result["body"])
if result["code"] != 0:
raise SystemExit(1)
finally:
connection.close()
Run the snippet from api-demo after switching the configuration. It reads one
reply and closes its connection without an automatic retry. For HTTP modes and
their separate settings, consult HttpServer.
Reliable stream over UDP
Direct UdpListener -> APIServer is unsupported. A length prefix does not
recover lost datagrams, reorder them, or reassemble a reliable response larger
than one datagram. Use the existing paired nodes:
TCP caller -> TcpListener -> TcpOverUdpClient -> UdpConnector
|
UDP transport
|
APIServer <- TcpOverUdpServer <- UdpListener
Starting again from the direct TCP example, replace api-listener with these
five nodes. Leave auth-db and management-api in the same nodes array:
[
{"name":"api-listener","type":"TcpListener",
"settings":{"address":"127.0.0.1","port":9080},"next":"reliable-client"},
{"name":"reliable-client","type":"TcpOverUdpClient","next":"udp-out"},
{"name":"udp-out","type":"UdpConnector",
"settings":{"address":"127.0.0.1","port":9081}},
{"name":"udp-in","type":"UdpListener",
"settings":{"address":"127.0.0.1","port":9081},"next":"reliable-server"},
{"name":"reliable-server","type":"TcpOverUdpServer","next":"management-api"}
]
Remove the HTTP node if adapting the HTTP variant instead, so it is not left unchained.
The Python client still connects to local TCP port 9080 with the same command.
It does not implement UDP, KCP or FEC. TcpOverUdpClient and TcpOverUdpServer
carry and reconstruct the stream; APIServer's four-byte prefix is still needed
to distinguish complete API messages within it.
Both sides run in one process over loopback here so the example is self-contained. For separate machines, put the TCP listener, reliable client and UDP connector on the caller side, and the UDP listener, reliable server, APIServer and target database on the server side. Change addresses appropriately. Logical API selectors refer to configurations loaded on the server. Match FEC settings when enabling FEC; see TcpOverUdpClient and TcpOverUdpServer for tuning and MTU rules.
These examples provide no transport encryption. Keep the local examples on loopback and use an appropriately protected transport for remote access. The same gateway resource limits, deadlines and uncertain-write rules apply to every composition.