343 lines
13 KiB
Markdown
343 lines
13 KiB
Markdown
# Proto Socket
|
|
|
|
Proto Socket은 여러 언어와 플랫폼 사이에서 양방향 통신을 하기 위한 바이너리 소켓 프로토콜 라이브러리다.
|
|
|
|
Protocol Buffers 직렬화, TCP 4바이트 빅엔디안 길이 프리픽스 프레이밍, WebSocket 바이너리 프레임, 타입 기반 메시지 라우팅, 요청-응답 상관관계, 내장 하트비트를 제공한다.
|
|
|
|
---
|
|
|
|
## 현재 상태
|
|
|
|
현재 프로토콜 `0.1`의 사용 가능 호환 구현은 Dart, Go, Kotlin, Python, TypeScript다.
|
|
|
|
이 프로젝트는 새 언어 대상을 늘리거나 패키지 registry에 배포하기 전 안정화와 유지보수 단계에 있다. 지금은 이 저장소를 Git 기반으로 소비하는 흐름을 기본 지원 방식으로 둔다. C#과 Swift는 계획된 대상이지만, 실제 수요가 생기기 전까지 구현을 미룬다.
|
|
|
|
프로토콜이나 구현을 바꿀 때는 아래 내부 로컬 검증 매트릭스를 안정성 gate로 사용한다. 현재 주 사용 방식은 Git 기반 소비이므로, CI/CD runner 연결은 기본 운영 모델이 아니며 이 프로젝트 규모와 배포 방식에서는 설계상 과하다.
|
|
|
|
```bash
|
|
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all
|
|
```
|
|
|
|
프로토콜 또는 공개 API 변경은 호환성 작업으로 다룬다. 관련 문서를 갱신하고, 필요한 경우 protobuf binding을 재생성하며, 전체 검증 매트릭스가 통과해야 한다.
|
|
|
|
---
|
|
|
|
## 설계 원칙
|
|
|
|
- core transport layer는 얇고 안정적으로 유지한다.
|
|
- 언어 간 통신에 필요한 최소 공통 기반만 제공한다.
|
|
- framing, serialization, routing, request-response correlation, heartbeat를 표준화한다.
|
|
- Protocol Buffers를 핵심 프로토콜 의존성으로 둔다. 다른 런타임 의존성은 native-first, 좁은 범위, 기능에 비례한 수준으로 유지한다.
|
|
- 애플리케이션 의미를 프로토콜 core에 넣지 않는다.
|
|
- auth, session, agent workflow, chat, game logic 같은 도메인 관심사는 상위 레이어 구현의 책임이다.
|
|
|
|
---
|
|
|
|
## 프로토콜
|
|
|
|
전체 wire format 명세는 [PROTOCOL.md](PROTOCOL.md)를 본다.
|
|
|
|
```text
|
|
[4-byte big-endian length] [PacketBase protobuf bytes]
|
|
```
|
|
|
|
WebSocket/WSS transport에서는 각 binary frame이 TCP length header 없이 `PacketBase` protobuf payload 하나를 담는다.
|
|
|
|
프로토콜 호환성은 언어별 package version과 분리해서 추적한다. 자세한 정책은 [VERSIONING.md](VERSIONING.md)를 본다. 패키지를 아직 registry에 배포하지 않더라도, 저장소에 포함된 구현은 문서화된 프로토콜 계약을 지켜야 한다.
|
|
|
|
---
|
|
|
|
## 구현체
|
|
|
|
| 언어 | 상태 | 경로 | 사용처 |
|
|
|------|------|------|--------|
|
|
| Dart | 사용 가능 | [dart/](dart/) | Flutter, Dart server |
|
|
| Kotlin | 사용 가능 | [kotlin/](kotlin/) | Android, JVM |
|
|
| Go | 사용 가능 | [go/](go/) | Server, tooling, scripting |
|
|
| TypeScript | 사용 가능 | [typescript/](typescript/) | Browser, Node.js |
|
|
| Python | 사용 가능 | [python/](python/) | Server, tooling, scripting |
|
|
| C# | 계획 / 보류 | `csharp/` | Unity, .NET |
|
|
| Swift | 계획 / 보류 | `swift/` | iOS, macOS |
|
|
|
|
새 언어 구현은 [PORTING_GUIDE.md](PORTING_GUIDE.md)와 [agent-ops/skills/project/add-proto-socket-crosstest-language/templates/](agent-ops/skills/project/add-proto-socket-crosstest-language/templates/)의 템플릿에서 시작한다. 동일 언어 테스트와 크로스 언어 테스트가 통과한 뒤에만 구현 상태를 사용 가능으로 표시한다.
|
|
|
|
현재 구현 상태는 이 README를 공개 snapshot으로 본다. 현재 작업 맥락은 [agent-roadmap/current.md](agent-roadmap/current.md)에서 선택하고, 큰 로드맵 방향은 [agent-roadmap/ROADMAP.md](agent-roadmap/ROADMAP.md)에 둔다.
|
|
|
|
---
|
|
|
|
## 주요 명령
|
|
|
|
| 목적 | 명령 | 비고 |
|
|
|------|------|------|
|
|
| 전체 로컬 검증 | `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all` | proto sync, 동일 언어 테스트, 사용 가능 구현의 크로스 언어 검증을 실행한다. |
|
|
| Proto schema sync 확인 | `tools/check_proto_sync.sh` | 언어별 proto copy가 허용된 언어 옵션을 제외하고 `proto/message_common.proto`와 일치하는지 확인한다. |
|
|
| Protobuf binding 재생성 | `tools/generate_proto.sh` | canonical proto schema를 바꾼 뒤 실행한다. |
|
|
| Dart 테스트 | `cd dart && dart pub get && dart test && dart compile js test/browser_ws_import_compile.dart -o /tmp/proto_socket_browser_ws_import_compile.js` | Dart VM/IO 테스트와 web import compile 확인 |
|
|
| Go 테스트 | `cd go && go test ./...` | Go 동일 언어 검증 |
|
|
| Kotlin 테스트 | `cd kotlin && ./gradlew test` | Kotlin 동일 언어 검증 |
|
|
| Python 테스트 | `cd python && python3 -m pytest -q` | Python 동일 언어 검증 |
|
|
| TypeScript 확인 | `cd typescript && npm run check && npm test` | TypeScript type check와 테스트 |
|
|
|
|
---
|
|
|
|
## 저장소 구조
|
|
|
|
| 경로 | 역할 |
|
|
|------|------|
|
|
| [proto/](proto/) | canonical protobuf schema 원본 |
|
|
| [dart/](dart/) | Dart / Flutter 구현체와 테스트 |
|
|
| [go/](go/) | Go 구현체, 테스트, Go 주도 crosstest |
|
|
| [kotlin/](kotlin/) | Kotlin / JVM 구현체와 crosstest |
|
|
| [python/](python/) | Python 구현체와 crosstest |
|
|
| [typescript/](typescript/) | TypeScript 구현체와 crosstest |
|
|
| [tools/](tools/) | proto 생성과 sync helper |
|
|
| [examples/](examples/) | 소비자 지향 예제 |
|
|
| [agent-ops/](agent-ops/) | 프로젝트 규칙, 로드맵, 반복 agent workflow |
|
|
|
|
---
|
|
|
|
## 빠른 시작: Dart
|
|
|
|
```dart
|
|
import 'package:proto_socket/proto_socket.dart';
|
|
|
|
// 1. message_common.proto에 메시지를 정의하고 protoc로 생성한다.
|
|
|
|
// 2. client를 구현한다.
|
|
class MyClient extends ProtobufClient {
|
|
MyClient(Socket socket) : super(socket, 30, 10, {
|
|
MyMessage.getDefault().info_.qualifiedMessageName: MyMessage.fromBuffer,
|
|
});
|
|
}
|
|
|
|
// 3. 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. server를 시작한다.
|
|
final server = MyServer();
|
|
await server.start();
|
|
|
|
// 5. 연결하고 메시지를 보낸다.
|
|
final socket = await Socket.connect('localhost', 9090);
|
|
final client = MyClient(socket);
|
|
await client.send(MyMessage()..text = 'hello');
|
|
```
|
|
|
|
---
|
|
|
|
## 메시지 타입 추가
|
|
|
|
canonical proto인 `proto/message_common.proto`를 수정한 뒤, 저장소에 포함된 모든 binding을 재생성한다.
|
|
|
|
```bash
|
|
tools/generate_proto.sh
|
|
tools/check_proto_sync.sh
|
|
```
|
|
|
|
Go와 Kotlin proto copy에는 `option go_package`, Java package/class option 같은 언어별 옵션만 추가로 허용한다. 메시지 schema가 `proto/message_common.proto`와 달라지면 `tools/check_proto_sync.sh`가 diff와 함께 실패한다.
|
|
|
|
---
|
|
|
|
## 빠른 시작: Go
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
protoSocket "git.toki-labs.com/toki/proto-socket/go"
|
|
"git.toki-labs.com/toki/proto-socket/go/packets"
|
|
)
|
|
|
|
func parserMap() protoSocket.ParserMap {
|
|
return protoSocket.ParserMap{
|
|
protoSocket.TypeNameOf(&packets.TestData{}): func(b []byte) (proto.Message, error) {
|
|
m := &packets.TestData{}
|
|
return m, proto.Unmarshal(b, m)
|
|
},
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
ctx := context.Background()
|
|
|
|
server := protoSocket.NewTcpServer("127.0.0.1", 9090, func(conn net.Conn) *protoSocket.TcpClient {
|
|
return protoSocket.NewTcpClient(conn, 30, 10, parserMap())
|
|
})
|
|
server.OnClientConnected = func(client *protoSocket.TcpClient) {
|
|
protoSocket.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 := protoSocket.DialTcp(ctx, "127.0.0.1", 9090, 30, 10, parserMap())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer client.Close()
|
|
|
|
res, err := protoSocket.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 구현체는 다음 표면도 제공한다.
|
|
|
|
- TCP: `NewTcpServer`, `DialTcp`, `NewTcpServerTLS`, `DialTcpTLS`
|
|
- WebSocket: `NewWsServer`, `DialWs`, `NewWsServerTLS`, `DialWss`
|
|
- Shared helpers: `Send`, `SendRequest`, `AddListenerTyped`, `AddRequestListenerTyped`, `Broadcast`
|
|
|
|
다른 저장소에서 이 모듈을 사용하는 복사 가능한 scaffold가 필요하면 [examples/go-module-consumer/](examples/go-module-consumer/)를 본다.
|
|
|
|
---
|
|
|
|
## 테스트 실행
|
|
|
|
개발과 문제 재현을 위한 로컬 명령은 이 문서에 정리한다. 반복 가능한 저장소 로컬 검증 진입점은 다음 명령이다.
|
|
|
|
```bash
|
|
bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all
|
|
```
|
|
|
|
이 명령은 proto schema sync, 동일 언어 테스트, 크로스 언어 검증을 실행하고, 실패가 있으면 재현 명령과 함께 PASS/FAIL 표를 출력한다. 더 작은 범위만 확인하려면 `--proto`, `--unit`, `--cross`를 사용한다.
|
|
|
|
외부 CI runner 연동은 이 저장소의 기본 운영 목표가 아니다. 현재 주 사용 방식은 Git 기반 소비와 내부 로컬 매트릭스 결과 확인이며, 이 범위에서는 별도 CI/CD runner 연결을 요구하지 않는다.
|
|
|
|
Proto schema sync:
|
|
|
|
```bash
|
|
tools/check_proto_sync.sh
|
|
```
|
|
|
|
동일 언어 검증:
|
|
|
|
```bash
|
|
cd dart
|
|
dart pub get
|
|
dart test
|
|
dart compile js test/browser_ws_import_compile.dart -o /tmp/proto_socket_browser_ws_import_compile.js
|
|
```
|
|
|
|
```bash
|
|
cd go
|
|
go test ./...
|
|
```
|
|
|
|
```bash
|
|
cd kotlin
|
|
./gradlew test
|
|
```
|
|
|
|
```bash
|
|
cd python
|
|
python3 -m pytest -q
|
|
```
|
|
|
|
```bash
|
|
cd typescript
|
|
npm run check
|
|
npm test
|
|
```
|
|
|
|
크로스 언어 검증:
|
|
|
|
```bash
|
|
cd go
|
|
go run ./crosstest/go_dart.go
|
|
go run ./crosstest/go_kotlin.go
|
|
go run ./crosstest/go_python.go
|
|
go run ./crosstest/go_typescript.go
|
|
```
|
|
|
|
```bash
|
|
cd dart
|
|
dart run crosstest/dart_go.dart
|
|
dart run crosstest/dart_kotlin.dart
|
|
dart run crosstest/dart_python.dart
|
|
dart run crosstest/dart_typescript.dart
|
|
```
|
|
|
|
```bash
|
|
cd kotlin
|
|
./gradlew run -PmainClass=com.tokilabs.proto_socket.crosstest.KotlinDartKt
|
|
./gradlew run -PmainClass=com.tokilabs.proto_socket.crosstest.KotlinGoKt
|
|
./gradlew run -PmainClass=com.tokilabs.proto_socket.crosstest.KotlinPythonKt
|
|
./gradlew run -PmainClass=com.tokilabs.proto_socket.crosstest.KotlinTypescriptKt
|
|
```
|
|
|
|
```bash
|
|
cd python
|
|
python3 crosstest/python_dart.py
|
|
python3 crosstest/python_go.py
|
|
python3 crosstest/python_kotlin.py
|
|
python3 crosstest/python_typescript.py
|
|
```
|
|
|
|
```bash
|
|
cd typescript
|
|
./node_modules/.bin/tsx crosstest/typescript_dart.ts
|
|
./node_modules/.bin/tsx crosstest/typescript_go.ts
|
|
./node_modules/.bin/tsx crosstest/typescript_kotlin.ts
|
|
./node_modules/.bin/tsx crosstest/typescript_python.ts
|
|
```
|
|
|
|
### Dart.io와 Dart.web E2E 매트릭스
|
|
|
|
위 표의 `Dart` 열은 Dart VM/IO client를 뜻한다. `Dart.web`은 별도 축이다. 브라우저 WS client만 가능하며, 브라우저에서는 TCP와 server 역할이 불가능하므로 해당 cell은 `N/A`다. WSS는 브라우저에서 self-signed test certificate 신뢰를 자동화하지 않았기 때문에 `Deferred`다.
|
|
|
|
| Server | Dart.io TCP | Dart.io WS | Dart.io TLS TCP | Dart.io WSS | Dart.web WS | Dart.web WSS | Dart.web TCP |
|
|
|---|---|---|---|---|---|---|---|
|
|
| Dart.io | Covered | Covered | Covered | Covered | Covered | Deferred | N/A |
|
|
| Go | Covered | Covered | Covered | Covered | Covered | Deferred | N/A |
|
|
| Kotlin | Covered | Covered | Covered | Covered | Covered | Deferred | N/A |
|
|
| Python | Covered | Covered | Covered | Covered | Covered | Deferred | N/A |
|
|
| TypeScript | Covered | Covered | Covered | Covered | Covered | Deferred | N/A |
|
|
|
|
각 Dart.web cell은 server-language runner가 해당 WS server를 띄우고, matching browser test file에 대해 `dart test -p chrome`을 실행하며, send-push와 request-response 시나리오를 검증한다.
|
|
|
|
```bash
|
|
cd dart && dart run crosstest/dart_web.dart
|
|
cd go && go run ./crosstest/go_dart_web.go
|
|
cd kotlin && ./gradlew run -PmainClass=com.tokilabs.proto_socket.crosstest.KotlinDartWebKt
|
|
cd python && python3 crosstest/python_dart_web.py
|
|
cd typescript && ./node_modules/.bin/tsx crosstest/typescript_dart_web.ts
|
|
```
|
|
|
|
---
|
|
|
|
## 작업 맥락
|
|
|
|
코드나 프로토콜을 변경하기 전에 [agent-ops/rules/project/rules.md](agent-ops/rules/project/rules.md)의 프로젝트 규칙과 해당 경로의 domain rule을 확인한다. 활성 로드맵 창은 [agent-roadmap/current.md](agent-roadmap/current.md)에 있다.
|
|
|
|
안정화 경계는 다음과 같다.
|
|
|
|
- 명시적인 호환성 작업 요청이 없으면 프로토콜 `0.1` 동작을 안정적으로 유지한다.
|
|
- 유지보수 모드 작업에는 bug fix, 문서 정정, 테스트 강화, 호환성을 유지하는 구현 수정이 포함될 수 있다.
|
|
- C#/Swift, package registry release, protocol/API 변경 작업은 구체적인 소비자 수요, 호환성 계획, 전체 검증 매트릭스 통과 조건이 있을 때 재개한다.
|
|
- 상위 `../oto` workflow가 결정되기 전까지 이 저장소에서 외부 CI/CD runner를 직접 연결하지 않는다.
|