Channels, transports & codecs
The communication substrate is deliberately separated from Services and Kernels:
Channels know nothing about services or kernels. Services and kernels know nothing about named pipes or TCP.
- Channel — a transport-neutral duplex byte/frame pipe. The high-performance path is built on
System.IO.Pipelines. - Transports — concrete connection factories:
DotBoxD.Transports.Tcp— cross-process / network.DotBoxD.Transports.NamedPipes— local-machine IPC.- an in-process channel is used by tests/benchmarks.
- Codecs — wire serialization behind a codec abstraction:
DotBoxD.Codecs.MessagePack— compact binary, zero-reflection with the generated formatters.
All of these target netstandard2.1 (Unity/IL2CPP friendly). A connection handshake negotiates protocol version, framing limits, and codec.
Why the stack is transport/codec-neutral (and when to pick each transport)
The problem it solves. Wiring a protocol straight to a socket type is the usual trap: the
serialization format, the frame layout, and if (tcp) … else if (pipe) … branching leak into the call
path, so moving from local IPC to the network — or from MessagePack to something else — means editing
the code that dispatches calls. DotBoxD avoids that by making the wire and the serializer
constructor-injected strategies. An
RpcPeer only ever sees two interfaces:
IRpcChannel
(the pipe) and
ISerializer
(the codec). It never names TCP, named pipes, or MessagePack — that single decision is the whole
mechanism behind the split quoted at the top of this page.
The payoff. You can swap the wire (named-pipe → TCP → WebSocket → in-process) and swap the
serialization without touching a single service contract. The three ways plugins talk to a host all
ride this one substrate and are equally indifferent to it: Services / RPC — one C#
contract compiles to a typed proxy + dispatcher, no hand-written marshaling and no runtime reflection on
the hot path; the query / event pipeline — server-side Where/Select filtering so only
matching, projected values cross the pipe; and pushdown — moving a loop next to the data
to turn N round-trips into one server-side batch.
Grounded aspects:
- A channel is just a duplex framed byte pipe — it has no notion of "service" or "method."
IRpcChannelexposes onlySendAsync,ReceiveAsync,IsConnected, andRemoteEndpoint; a zero-length receive signals "remote closed." Service names, method names, request IDs, and cancellation all live above this line in the envelope, never in the channel. - Framing is shared, not reinvented per transport. The wire frame —
[4B total length][4B messageId][1B MessageType][body]— is defined once inMessageFramerand reused by every stream-backed transport. Named pipes reuseStreamConnectiondirectly, so named-pipe traffic gets the same length validation, serialized sends, pooled receive buffers, and clean EOF behavior as every other stream-backed DotBoxD connection; TCP validates outgoing frames with the sameMessageFramerconstants so a malformed frame is rejected locally rather than differing by transport. - Symmetry falls out of the channel being duplex. The four core
MessageTypes (Request/Response/Error/Cancel) encode direction independently of who sent the frame, so responses flow back over the same pipe and one read loop can demux both directions. The "client = get-only / server = provide-only" asymmetry is peer configuration, not a channel or transport property — see peer-model. - Adding a transport = implement three interfaces, change zero contracts. The
WebSocket guide is proof by construction: it is not a shipped
package but a walkthrough that implements
ITransport,IServerTransport, andIRpcChannel— the same[DotBoxDService]interfaces and generated proxies then run over a transport the framework never shipped. - Codec neutrality is the same trick applied to bytes.
RpcPeer/RpcHosttake anISerializer; swapping codecs is passing a different one. The MessagePack codec is zero-reflection by design — it composes DotBoxD's own binary formatters ahead of the standard resolvers and hardens the boundary withMessagePackSecurity.UntrustedData(validate untrusted input at the edge). ACreateUnityCompatible()variant swaps in a contractless resolver for attribute-free DTOs. Zero runtime reflection is what makes both the codec swap and the transport swap safe under Unity/IL2CPP AOT (see unity-integration); thenetstandard2.1target above is the same story.
When to reach for each transport
| Transport | Reach for it when | Notes |
|---|---|---|
Named pipes (DotBoxD.Transports.NamedPipes) |
Same machine, cross-process IPC | Separate package so TCP-only hosts take no pipe dependency; duplex, so one pipe can both serve and call. See named-pipe transport. |
TCP (DotBoxD.Transports.Tcp) |
Cross-host / over the network | Default quick-start transport; faces untrusted networks, so TcpConnection ships a slow-loris defense (DefaultFrameReadIdleTimeout, 30s). |
| WebSocket (you implement it) | Browser clients, Unity WebGL, or HTTP-based connectivity | Not shipped — implement the three interfaces from the WebSocket guide. |
In-process (in-memory IRpcChannel) |
Tests and benchmarks, no OS transport | Lives in test/benchmark code, not a shipped src/ package. |
Backpressure and the near-zero-allocation ValueTask<T> path are peer options, orthogonal to the
transport you pick — see performance.
For deeper transport material (named pipes, WebSocket extension, performance, design rationale) see the
legacy RPC docs under docs/channels/.
Roadmap: extracting the transport-neutral abstractions into a dedicated
DotBoxD.Channels/DotBoxD.Channels.Abstractionspackage is tracked in follow-up-issues.
Next step
You have finished the concept layer. To see channels, services, and kernels working together in one running program, continue to the GameServer walkthrough.