diff options
| author | bt <bt@rctt.net> | 2026-03-09 01:11:13 +0100 |
|---|---|---|
| committer | bt <bt@rctt.net> | 2026-03-09 22:52:52 +0100 |
| commit | 077dde28324f42b96239075ada289a58197dc951 (patch) | |
| tree | 4889aee82e8ea8efeeb141f2a98aeb089fff1798 | |
| parent | cfcb226d3834c00414f4aa57b8f94060b45bb072 (diff) | |
| download | solec-077dde28324f42b96239075ada289a58197dc951.tar.gz solec-077dde28324f42b96239075ada289a58197dc951.zip | |
Improve protocol format
| -rw-r--r-- | PROTOCOL.md | 96 | ||||
| -rw-r--r-- | cmd/client/main.go | 14 | ||||
| -rw-r--r-- | cmd/daemon/main.go | 16 | ||||
| -rw-r--r-- | cmd/test/main.go | 34 | ||||
| -rw-r--r-- | core/data.go | 192 | ||||
| -rw-r--r-- | core/data_test.go | 35 | ||||
| -rw-r--r-- | core/network.go | 65 | ||||
| -rw-r--r-- | core/payload.go | 128 | ||||
| -rw-r--r-- | go.mod | 2 | ||||
| -rw-r--r-- | go.sum | 2 |
10 files changed, 406 insertions, 178 deletions
diff --git a/PROTOCOL.md b/PROTOCOL.md index b481a0e..895d1df 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -1,45 +1,91 @@ +<!--- --------------------------------------------------------------------- --> + # SOLEC protocol specification -## Data format +* Payload can contain multiple numeric types or binary data in a single frame +* Numeric types are big endian +* Numeric types names are same as in [Go](https://go.dev/ref/spec#Numeric_types) +* See [test payload](#Test) for an example of complex data structure -Everything is big endian. `text` is always null terminated UTF-8. -Every packet starts with `uint8` denoting type of the rest of remaining data. +## Frame -## Event types +| Type | Description | +|----------|----------------| +| `uint8` | Payload type | +| `uint16` | Payload length | +| `[]any` | Payload | -| Type | Name | -|------|-----------| -| 0x01 | Handshake | -| 0x02 | Ping | -| 0x03 | Pong | -| 0x04 | Message | -| 0xFF | Test | -## Events +## Data types -### Handshake +### Numeric types + +* `uint8` +* `uint16` +* `uint32` +* `uint64` + +### `binary` + +| Type | Description | +|--------|----------------| +| uint16 | Data length | +| binary | Data | + +### `string` + +UTF-8 encoded string encapsulated in `bianry` type. -Sent after estabilishing TCP connection. First by the client and then by the server. -If one of the sides uses different protocol version connection should be aborted. +### `time` + +[Unix timestamp](https://en.wikipedia.org/wiki/Unix_time) stored in `uint16`. +Timezone is always UTC. + +# Payloads + +## Payload prefixes + +| Type | Name | +|------|--------------| +| 0x01 | Handshake | +| 0x02 | Ping | +| 0x03 | Pong | +| 0x04 | Message | +| 0xFF | Test | + +## Payload types + +### Handshake -| Type | Name | Description | -|-------|------|------------------| -| uint8 | ver | Protocol version | +| Type | Name | Description | +|--------|---------------------|----------------------------------------------------------| +| uint8 | ProtocolVersionMajor | Bumped if changes are incompatible with previous version | +| uint8 | PrococolVersionMinor | Bumber if changes are backwards compatible | ### Ping -Empty. +Can be send by client or server. Receiver shoudl reply with `Pong`. ### Pong -`time` field contains Unix timestamp generated just before sending the packet. -Timezone should always be UTC. +Reply to `ping`. -| Type | Name | Description | -|--------|------|------------------| -| uint64 | time | Unix timestamp | +| Type | Name | Description | +|--------|------|-------------------------| +| uint64 | Time | Set just before sending | ### Test -Event type reserved for testing. Does not have defined structure.
\ No newline at end of file +Used for testing purposes. Can change at any time. + +| Type | Name | Description | +|--------|-------|-------------| +| uint8 | Num1 | | +| time | Time1 | | +| string | Str1 | | +| uint16 | Num2 | | +| binary | Bin1 | | +| uint32 | Num3 | | +| string | Str2 | | +| uint64 | Num4 | | diff --git a/cmd/client/main.go b/cmd/client/main.go index a229632..8bb1c24 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -23,17 +23,19 @@ func main() { } defer conn.Close() - go ping(conn) - if err := core.Loop(conn); err != nil { - log.Fatal("event loop error ", err) - } + ping(conn) } func ping(conn net.Conn) { for { log.Print("ping") - p := core.Ping{} - core.Send(conn, core.TypePing, p) + data, err := core.Encode(core.Ping{}) + if err != nil { + panic(err) + } + if _, err := conn.Write(data); err != nil { + panic(err) + } time.Sleep(time.Second) } } diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index b344caf..b55cc69 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -31,21 +31,9 @@ func listen() error { if err != nil { log.Print("cannot accept connection: ", err) } - go handle(conn) - } -} - -func handle(conn net.Conn) { - log.Print("received connection from: ", conn.RemoteAddr()) - hs := core.Handshake{Version: 0x0} - - if err := core.Send(conn, core.TypeHandshake, hs); err != nil { - log.Print("cannot send handshake ", err) - return - } + log.Print("received connection from: ", conn.RemoteAddr()) - if err := core.Loop(conn); err != nil { - log.Fatal("event loop error ", err) + go core.ReadConnection(conn) } } diff --git a/cmd/test/main.go b/cmd/test/main.go new file mode 100644 index 0000000..e580b63 --- /dev/null +++ b/cmd/test/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "time" + + "git.sr.ht/~rctt/solecd/core" +) + +func main() { + in := core.Test{ + Num1: 1, + Time1: time.Now(), + Str1: "test string", + Num2: 2, + Bin1: []byte{0x01, 0x02, 0x03}, + Num3: 3, + Str2: "こんにちは", + Num4: 4, + } + + data, err := core.Encode(in) + if err != nil { + panic(err) + } + + fmt.Printf("% X\n", data) + out, err := core.Decode(data) + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", out) +} diff --git a/core/data.go b/core/data.go index ddf1db8..16775db 100644 --- a/core/data.go +++ b/core/data.go @@ -1,122 +1,164 @@ package core import ( + "bytes" "encoding/binary" "fmt" "io" - "strings" "time" ) -type Marshaler interface { - Marshal() []any +type Wrapper interface { + Wrap() (PayloadType, []any) } -type DataType uint8 +type PayloadType uint8 const ( - TypeUnknown DataType = 0x00 - TypeHandshake = 0x01 - TypePing = 0x02 - TypePong = 0x03 - TypeTest = 0xFF + PayloadUnknown PayloadType = 0x00 + PayloadHandshake = 0x01 + PayloadPing = 0x02 + PayloadPong = 0x03 + PayloadMessage = 0x04 + PayloadTest = 0xFF ) -func ReadDataType(r io.Reader) (DataType, error) { - var data uint8 - if err := read(r, &data); err != nil { - return TypeUnknown, err - } - - dType := DataType(data) - - return dType, nil +type Frame struct { + Length uint16 + Type PayloadType + Payload []any } -type Handshake struct { - Version uint8 +type Binary struct { + Length uint16 + Payload []byte } -func (t Handshake) Marshal() []any { - return []any{t.Version} -} +func Encode(w Wrapper) ([]byte, error) { + payloadType, payload := w.Wrap() + buf := bytes.NewBuffer(make([]byte, 3)) + + for _, p := range payload { + switch v := p.(type) { + case string: + err := binary.Write(buf, binary.BigEndian, uint16(len(v))) + if err != nil { + return []byte{}, fmt.Errorf("cannot encode string length: %v", err) + } + + _, err = buf.WriteString(v) + if err != nil { + return []byte{}, fmt.Errorf("cannot encode string: %v", err) + } + + case []byte: + err := binary.Write(buf, binary.BigEndian, uint16(len(v))) + if err != nil { + return []byte{}, fmt.Errorf("cannot encode byte slice length: %v", err) + } + + _, err = buf.Write(v) + if err != nil { + return []byte{}, fmt.Errorf("cannot encode byte slice: %v", err) + } + + case time.Time: + err := binary.Write(buf, binary.BigEndian, uint64(v.Unix())) + if err != nil { + return []byte{}, fmt.Errorf("cannot encode time: %v", err) + } + + default: + err := binary.Write(buf, binary.BigEndian, v) + if err != nil { + return []byte{}, fmt.Errorf("cannot encode: %v", err) + } + } + } -func ReadHandshake(r io.Reader) (Handshake, error) { - var t Handshake - err := read(r, &t.Version) - return t, err -} + frame := buf.Bytes() + frame[0] = uint8(payloadType) + binary.BigEndian.PutUint16(frame[1:], uint16(len(frame)-3)) -type Test struct { - Message string + return frame, nil } -func (t Test) Marshal() []any { - return []any{append([]byte(t.Message), 0x0)} -} +func Decode(buf io.Reader) (any, error) { + var pTypeByte uint8 -func ReadTest(r io.Reader) (Test, error) { - var t Test - err := readString(r, &t.Message) - return t, err -} + err := binary.Read(buf, binary.BigEndian, &pTypeByte) + if err != nil { + return nil, fmt.Errorf("cannot read payload type: %v", err) + } -type Ping struct{} + pType := PayloadType(pTypeByte) -func (t Ping) Marshal() []any { - return []any{} -} + var pLen uint16 + err = binary.Read(buf, binary.BigEndian, &pLen) + if err != nil { + return nil, fmt.Errorf("cannot read payload length: %v", err) + } -type Pong struct { - Timestamp time.Time + switch pType { + case PayloadHandshake: + return DecodeHandshake(buf) + case PayloadPing: + case PayloadPong: + case PayloadMessage: + case PayloadTest: + return DecodeTest(buf) + default: + return nil, fmt.Errorf("invalid payload type: %v", pType) + } + + return pType, nil } -func (t Pong) Marshal() []any { - return []any{uint64(t.Timestamp.Unix())} +func decodeNumeric(buf io.Reader, ptr any) error { + err := binary.Read(buf, binary.BigEndian, ptr) + if err != nil { + return fmt.Errorf("cannot decode: %v", err) + } + return nil } -func ReadPong(r io.Reader) (Pong, error) { - var ( - t Pong - timestamp uint64 - ) +func decodeTime(buf io.Reader, ptr *time.Time) error { + var timeBuf uint64 + err := binary.Read(buf, binary.BigEndian, &timeBuf) + if err != nil { + return fmt.Errorf("cannot decode time: %v", err) + } - err := read(r, ×tamp) - t.Timestamp = time.Unix(int64(timestamp), 0) - return t, err + *ptr = time.Unix(int64(timeBuf), 0) + return nil } -// +func decodeBin(buf io.Reader, ptr *[]byte) error { + var payloadLen uint16 -func readString(r io.Reader, ptr *string) error { - var ( - sb strings.Builder - buf byte - ) + err := binary.Read(buf, binary.BigEndian, &payloadLen) + if err != nil { + return fmt.Errorf("cannot decode payload length: %v", err) + } - for { - if err := read(r, &buf); err != nil { - return err - } + payload := make([]byte, payloadLen) - if buf == 0x0 { - break - } - - if err := sb.WriteByte(buf); err != nil { - fmt.Errorf("cannot write byte to string buffer: %v", err) - } + _, err = buf.Read(payload) + if err != nil { + return fmt.Errorf("cannot decode payload: %v", err) } - *ptr = sb.String() + *ptr = payload return nil } -func read(r io.Reader, ptr any) error { - err := binary.Read(r, binary.BigEndian, ptr) +func decodeString(buf io.Reader, ptr *string) error { + var strBytes []byte + err := decodeBin(buf, &strBytes) if err != nil { - return fmt.Errorf("cannot read: %v", err) + return err } + *ptr = string(strBytes) return nil } diff --git a/core/data_test.go b/core/data_test.go new file mode 100644 index 0000000..1275ef4 --- /dev/null +++ b/core/data_test.go @@ -0,0 +1,35 @@ +package core + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestEncoder(t *testing.T) { + in := Test{ + Num1: 1, + Time1: time.Now().Truncate(time.Second), + Str1: "test string", + Num2: 2, + Bin1: []byte{0x01, 0x02, 0x03}, + Num3: 3, + Str2: "こんにちは", + Num4: 4, + } + + data, err := Encode(in) + if err != nil { + t.Error(err) + } + + out, err := Decode(data) + if err != nil { + t.Error(err) + } + + if diff := cmp.Diff(in, out); diff != "" { + t.Errorf("structs are different (-in +out):\n%s", diff) + } +} diff --git a/core/network.go b/core/network.go index c662981..c182e7f 100644 --- a/core/network.go +++ b/core/network.go @@ -1,75 +1,24 @@ package core import ( - "encoding/binary" "fmt" - "io" - "log" "net" - "time" ) -func Send(conn net.Conn, dataType DataType, data Marshaler) error { - packet := []any{uint8(dataType)} - packet = append(packet, data.Marshal()...) - - for _, v := range packet { - err := binary.Write(conn, binary.BigEndian, v) - if err != nil { - return fmt.Errorf("cannot send: %v", err) - } - } - - return nil -} - -func Read(r io.Reader) (any, error) { - dType, err := ReadDataType(r) - if err != nil { - return nil, fmt.Errorf("cannot read data type", err) - } - - switch dType { - case TypeUnknown: - return nil, fmt.Errorf("cannot read data type 0x00 (TypeUnknown)") - case TypeHandshake: - return ReadHandshake(r) - case TypePing: - return Ping{}, nil - case TypePong: - return ReadPong(r) - case TypeTest: - return ReadTest(r) - default: - return nil, fmt.Errorf("unsupported type: %v", dType) - } -} - -func Loop(conn net.Conn) error { +func ReadConnection(conn net.Conn) { for { - d, err := Read(conn) + payload, err := Decode(conn) if err != nil { - return fmt.Errorf("cannot read data: %v", err) + panic(err) } - if err := handleEvent(d, conn); err != nil { - return fmt.Errorf("cannot handle event: %v", err) + if err := handle(payload); err != nil { + panic(err) } } } -func handleEvent(data any, conn net.Conn) error { - switch v := data.(type) { - case Handshake: - case Ping: - log.Print("received ping") - return Send(conn, TypePong, Pong{time.Now()}) - case Pong: - log.Print("received pong ", v.Timestamp) - case Test: - default: - return fmt.Errorf("unknown type") - } - +func handle(payload any) error { + fmt.Println(payload) return nil } diff --git a/core/payload.go b/core/payload.go new file mode 100644 index 0000000..0711040 --- /dev/null +++ b/core/payload.go @@ -0,0 +1,128 @@ +package core + +import ( + "io" + "time" +) + +type Handshake struct { + Major, Minor uint8 +} + +func (h Handshake) Wrap() (PayloadType, []any) { + return PayloadHandshake, []any{ + h.Major, h.Minor, + } +} + +func DecodeHandshake(buf io.Reader) (Handshake, error) { + var h Handshake + + err := decodeNumeric(buf, &h.Major) + if err != nil { + return h, err + } + + err = decodeNumeric(buf, &h.Minor) + if err != nil { + return h, err + } + + return h, nil +} + +type Ping struct{} + +func (p Ping) Wrap() (PayloadType, []any) { + return PayloadPing, []any{} +} + +type Pong struct { + Timestamp time.Time +} + +func (p Pong) Wrap() (PayloadType, []any) { + return PayloadPong, []any{ + p.Timestamp, + } +} + +func DecodePong(buf io.Reader) (Pong, error) { + var p Pong + err := decodeTime(buf, &p.Timestamp) + if err != nil { + return p, err + } + + return p, nil +} + +type Test struct { + Num1 uint8 + Time1 time.Time + Str1 string + Num2 uint16 + Bin1 []byte + Num3 uint32 + Str2 string + Num4 uint64 +} + +func (h Test) Wrap() (PayloadType, []any) { + return PayloadTest, []any{ + h.Num1, + h.Time1, + h.Str1, + h.Num2, + h.Bin1, + h.Num3, + h.Str2, + h.Num4, + } +} + +func DecodeTest(buf io.Reader) (Test, error) { + var t Test + + err := decodeNumeric(buf, &t.Num1) + if err != nil { + return t, err + } + + err = decodeTime(buf, &t.Time1) + if err != nil { + return t, err + } + + err = decodeString(buf, &t.Str1) + if err != nil { + return t, err + } + + err = decodeNumeric(buf, &t.Num2) + if err != nil { + return t, err + } + + err = decodeBin(buf, &t.Bin1) + if err != nil { + return t, err + } + + err = decodeNumeric(buf, &t.Num3) + if err != nil { + return t, err + } + + err = decodeString(buf, &t.Str2) + if err != nil { + return t, err + } + + err = decodeNumeric(buf, &t.Num4) + if err != nil { + return t, err + } + + return t, nil +} @@ -1,3 +1,5 @@ module git.sr.ht/~rctt/solecd go 1.25.0 + +require github.com/google/go-cmp v0.7.0 @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= |
