- Add WebSocket binary frame support to protocol specification - Update README and PROTOCOL.md to reflect dual TCP/WebSocket transport - Add Go implementation with TCP, WebSocket, and heartbeat support - Include .claude settings configuration
181 lines
4.5 KiB
Markdown
181 lines
4.5 KiB
Markdown
# Toki Socket
|
|
|
|
Binary socket protocol library for bidirectional, heterogeneous communication across languages and platforms.
|
|
|
|
Built on Protocol Buffers with TCP length-prefixed framing, WebSocket binary frames, type-based message routing, request-response correlation, and built-in heartbeat.
|
|
|
|
---
|
|
|
|
## Design Principles
|
|
|
|
- Keep the core transport layer thin and stable
|
|
- Provide only the minimum common foundation for cross-language communication
|
|
- Standardize framing, serialization, routing, request-response correlation, and heartbeat
|
|
- Do not embed application semantics into the protocol core
|
|
- Domain-specific concerns such as auth, session, agent workflow, chat features, and game logic belong in upper-layer implementations
|
|
|
|
---
|
|
|
|
## Protocol
|
|
|
|
See [PROTOCOL.md](PROTOCOL.md) for the full wire format specification.
|
|
|
|
```
|
|
[4-byte big-endian length] [PacketBase protobuf bytes]
|
|
```
|
|
|
|
For WebSocket/WSS transports, each binary frame contains one `PacketBase` protobuf payload without the TCP length header.
|
|
|
|
---
|
|
|
|
## Implementations
|
|
|
|
| Language | Status | Path | Use case |
|
|
|----------|--------|------|----------|
|
|
| Dart | Available | [dart/](dart/) | Flutter, Dart server |
|
|
| C# | Planned | `csharp/` | Unity, .NET |
|
|
| Kotlin | Planned | `kotlin/` | Android |
|
|
| Swift | Planned | `swift/` | iOS, macOS |
|
|
| Go | Available | [go/](go/) | Server, tooling, scripting |
|
|
| Python | Planned | `python/` | Server, tooling, scripting |
|
|
| Rust | Planned | `rust/` | High-performance server, embedded |
|
|
|
|
---
|
|
|
|
## Quick Start (Dart)
|
|
|
|
```dart
|
|
import 'package:toki_socket/toki_socket.dart';
|
|
|
|
// 1. Define your message in message_common.proto, generate with protoc
|
|
|
|
// 2. Implement a client
|
|
class MyClient extends ProtobufClient {
|
|
MyClient(Socket socket) : super(socket, 30, 10, {
|
|
MyMessage.getDefault().info_.qualifiedMessageName: MyMessage.fromBuffer,
|
|
});
|
|
}
|
|
|
|
// 3. Implement a server
|
|
class MyServer extends ProtobufServer {
|
|
MyServer() : super('0.0.0.0', 9090, (socket) => MyClient(socket));
|
|
|
|
@override
|
|
void onClientConnected(ProtobufClient client) {
|
|
client.addListener<MyMessage>((msg) => print('Received: ${msg}'));
|
|
}
|
|
}
|
|
|
|
// 4. Start
|
|
final server = MyServer();
|
|
await server.start();
|
|
|
|
// 5. Connect and send
|
|
final socket = await Socket.connect('localhost', 9090);
|
|
final client = MyClient(socket);
|
|
await client.send(MyMessage()..text = 'hello');
|
|
```
|
|
|
|
---
|
|
|
|
## Adding Message Types
|
|
|
|
Edit `dart/lib/src/packets/message_common.proto` and regenerate:
|
|
|
|
```bash
|
|
cd dart
|
|
dart pub global activate protoc_plugin
|
|
protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto
|
|
```
|
|
|
|
For Go, keep `go/packets/message_common.proto` in sync with the canonical Dart proto. The Go copy includes `option go_package`, then regenerate from the Go module root:
|
|
|
|
```bash
|
|
cd go
|
|
protoc --go_out=. --go_opt=paths=source_relative packets/message_common.proto
|
|
```
|
|
|
|
---
|
|
|
|
## Quick Start (Go)
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
toki "toki-labs.com/toki_socket/go"
|
|
"toki-labs.com/toki_socket/go/packets"
|
|
)
|
|
|
|
func parserMap() toki.ParserMap {
|
|
return toki.ParserMap{
|
|
toki.TypeNameOf(&packets.TestData{}): func(b []byte) (proto.Message, error) {
|
|
m := &packets.TestData{}
|
|
return m, proto.Unmarshal(b, m)
|
|
},
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
ctx := context.Background()
|
|
|
|
server := toki.NewTcpServer("127.0.0.1", 9090, func(conn net.Conn) *toki.TcpClient {
|
|
return toki.NewTcpClient(conn, 30, 10, parserMap())
|
|
})
|
|
server.OnClientConnected = func(client *toki.TcpClient) {
|
|
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](
|
|
&client.Communicator,
|
|
func(req *packets.TestData) (*packets.TestData, error) {
|
|
return &packets.TestData{Index: req.GetIndex(), Message: "echo: " + req.GetMessage()}, nil
|
|
},
|
|
)
|
|
}
|
|
if err := server.Start(ctx); err != nil {
|
|
panic(err)
|
|
}
|
|
defer server.Stop()
|
|
|
|
client, err := toki.DialTcp(ctx, "127.0.0.1", 9090, 30, 10, parserMap())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer client.Close()
|
|
|
|
res, err := toki.SendRequestTyped[*packets.TestData, *packets.TestData](
|
|
&client.Communicator,
|
|
&packets.TestData{Index: 1, Message: "hello"},
|
|
2*time.Second,
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
println(res.GetMessage())
|
|
}
|
|
```
|
|
|
|
Go also provides:
|
|
|
|
- TCP: `NewTcpServer`, `DialTcp`, `NewTcpServerTLS`, `DialTcpTLS`
|
|
- WebSocket: `NewWsServer`, `DialWs`, `NewWsServerTLS`, `DialWss`
|
|
- Shared helpers: `Send`, `SendRequest`, `AddListenerTyped`, `AddRequestListenerTyped`, `Broadcast`
|
|
|
|
---
|
|
|
|
## Running Tests
|
|
|
|
```bash
|
|
cd dart
|
|
dart pub get
|
|
dart test
|
|
```
|
|
|
|
```bash
|
|
cd go
|
|
go test ./...
|
|
```
|