# 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 `PacketBase` protobuf payload - Value of `0`: reserved / no-op, receiver clears buffer ### PacketBase ```protobuf 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 ```protobuf 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 `heartbeatIntervalTime` seconds → send `HeartBeat` - If no response within `heartbeatWaitTime` seconds → `onDisconnected()` → `dispose()` - Only the initiating side responds to HeartBeat echo (ping-pong prevention via `_waitingHeartbeatResponse` flag) --- ## 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 | | Python | `descriptor.name` from `MessageClass.DESCRIPTOR` — verify matches | | Rust | `M::default().descriptor_dyn().name().to_string()` (protobuf crate) — verify matches | **Important**: Verify typeName consistency across languages before connecting heterogeneous clients. --- ## nonce - Starts at `1` per connection - Increments by `1` on every `send()` 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/` | | Python | Planned | `python/` | | Rust | Planned | `rust/` | --- ## Proto Source `dart/lib/src/packets/message_common.proto` is the canonical packet definition. All language implementations must generate bindings from this file.