proto-socket/PROTOCOL.md

139 lines
4.8 KiB
Markdown

# 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.
---
## Transport별 Wire Format
### TCP / SSL+TCP
```
┌──────────────────────────────────────────┐
│ 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
### WebSocket / WSS
```
┌──────────────────────────────────────────┐
│ PacketBase (protobuf, N bytes) │ ← typeName + nonce + data
└──────────────────────────────────────────┘
```
- Length 헤더 없음 — WebSocket 프로토콜이 메시지 경계를 보장한다
- Binary frame 사용 (text frame 아님)
- 연결: `ws://host:port/path` (plain) / `wss://host:port/path` (TLS)
### 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.