summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbt <bt@rctt.net>2026-03-09 01:11:13 +0100
committerbt <bt@rctt.net>2026-03-09 22:52:52 +0100
commit077dde28324f42b96239075ada289a58197dc951 (patch)
tree4889aee82e8ea8efeeb141f2a98aeb089fff1798
parentcfcb226d3834c00414f4aa57b8f94060b45bb072 (diff)
downloadsolec-077dde28324f42b96239075ada289a58197dc951.tar.gz
solec-077dde28324f42b96239075ada289a58197dc951.zip
Improve protocol format
-rw-r--r--PROTOCOL.md96
-rw-r--r--cmd/client/main.go14
-rw-r--r--cmd/daemon/main.go16
-rw-r--r--cmd/test/main.go34
-rw-r--r--core/data.go192
-rw-r--r--core/data_test.go35
-rw-r--r--core/network.go65
-rw-r--r--core/payload.go128
-rw-r--r--go.mod2
-rw-r--r--go.sum2
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, &timestamp)
- 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
+}
diff --git a/go.mod b/go.mod
index e1aaa45..a4a5a0f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,5 @@
module git.sr.ht/~rctt/solecd
go 1.25.0
+
+require github.com/google/go-cmp v0.7.0
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..40e761a
--- /dev/null
+++ b/go.sum
@@ -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=