- Protobuf-based binary TCP socket library - ProtobufClient / ProtobufServer abstract classes with heartbeat - Type-based message routing via Communicator - 4-byte big-endian length-prefixed framing protocol - 8 integration tests (all passing) - PROTOCOL.md wire format specification - VSCode launch.json for test runner Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 KiB
4 KiB
Toki Socket Protocol
Binary TCP protocol using Protocol Buffers for message framing and type-based routing. Designed for bidirectional, heterogeneous communication across languages and platforms.
Wire Format
┌──────────────────────────────────────────┐
│ Header (4 bytes, big-endian int32) │ ← PacketBase payload length
├──────────────────────────────────────────┤
│ PacketBase (protobuf, N bytes) │ ← typeName + nonce + data
└──────────────────────────────────────────┘
Header
- 4 bytes, big-endian signed int32
- Value: byte length of the following
PacketBaseprotobuf payload - Value of
0: reserved / no-op, receiver clears buffer
PacketBase
message PacketBase {
string typeName = 1; // fully-qualified protobuf message name
int32 nonce = 2; // monotonically increasing per-connection counter
bytes data = 3; // serialized inner message bytes
}
| Field | Description |
|---|---|
typeName |
GeneratedMessage.info_.qualifiedMessageName — language-agnostic routing key |
nonce |
Starts at 1, increments by 1 per send() call per connection |
data |
Protobuf-serialized bytes of the inner message |
Built-in Messages
HeartBeat
message HeartBeat {}
Automatically handled by the framework. Applications do not need to register or handle this manually.
Heartbeat Lifecycle
Side A Side B
│ │
│── (no activity for intervalTime) ──────▶│
│ send(HeartBeat) │
│ │── onHeartBeat() called
│ │ send(HeartBeat) [response]
│◀─────────────────────────────────────────│
│ onHeartBeat() called │
│ _waitingHeartbeatResponse = false │
│ timer reset │
- After any received message → heartbeat interval timer resets
- If no data received within
heartbeatIntervalTimeseconds → sendHeartBeat - If no response within
heartbeatWaitTimeseconds →onDisconnected()→dispose() - Only the initiating side responds to HeartBeat echo (ping-pong prevention via
_waitingHeartbeatResponseflag)
typeName Convention
Uses GeneratedMessage.info_.qualifiedMessageName on the send side.
On the receive side, use the same value as the registration key.
| Language | Registration key |
|---|---|
| Dart | T.toString() (equals qualified name for top-level proto messages) |
| C# | typeof(T).Name — verify matches proto qualified name |
| Kotlin | T::class.simpleName — verify matches |
| Swift | String(describing: T.self) — verify matches |
Important: Verify typeName consistency across languages before connecting heterogeneous clients.
nonce
- Starts at
1per connection - Increments by
1on everysend()call - Currently used as a message ID; request-response correlation can be built on top
Example Packet (hex)
Sending HeartBeat {}:
00 00 00 05 ← header: PacketBase length = 5 bytes
0A 09 48 65 61 72 74 42 65 61 74 ← PacketBase { typeName: "HeartBeat" }
Multi-language Implementations
| Language | Status | Path |
|---|---|---|
| Dart | Available | dart/ |
| C# (Unity) | Planned | csharp/ |
| Kotlin | Planned | kotlin/ |
| Swift | Planned | swift/ |
Proto Source
dart/lib/src/packets/message_common.proto is the canonical packet definition.
All language implementations must generate bindings from this file.