January 7, 2023 · go and net

binproto implements low-level support for binary-based two-way communication protocols. You can use it to create your own low-latency binary protocols for things like game networking or file transfer.

The Transmission Control Protocol (TCP) provides reliable delivery of a stream of bytes between two hosts. But it's the responsibility of the application level protocols to parse incoming data (either in text or bytes) into an application-specific message.

Go's standard library provides net/textproto for implementing text protocols (in the style of HTTP, SMTP) in a convenient fashion. However, for binary protocols, there is really no consensus on what might be the most generic way for dividing a long stream of bytes into discrete messages.

Message format

binproto internally implements a streaming state machine borrowed from the hypercore wire protocol. Over the wire each message is packed in the following format:

╔──────────────────────────────────────────────╗
│ length | channel ID × channel type │ payload │
╚──────────────────────────────────────────────╝
           └─ 60-bits   └─ 4-bits

This simple technique, which is performed basically by writing the size of each message to a stream before a message itself, is called length-prefix framing.

Each message starts with a header which is a varint encoded unsigned 64-bit integer and consists of a channel ID (first 60-bits) and a channel type (last 4-bits), the rest is the body of a message.

Buffering

binproto uses an internal buffer which allocates 4096 bytes by default, meaning that it will process what's inside the buffer as long its size is equal or greater than this value; which is a sensible default for many applications. You can adjust this value for optimal performance if your protocol requires larger (or smaller) chunks.

See the full API documentation at pkg.go.dev

Example: Echo

To start receiving and sending messages, all we really need to do is to pipe a net.Conn into a binproto.Conn instance. Once a connection is established, we can call ReadMessage() and Send() methods on a Conn instance to read and send messages.

package main

import (
    "fmt"
    "log"
    "net"
    "time"

    "github.com/onur1/binproto"
)

func main() {
    s := &server{}

    time.AfterFunc(time.Millisecond*1, func() {
        c, err := binproto.Dial("tcp", ":4242")
        if err != nil {
            log.Fatal(err)
        }

        go func() {
            for {
                msg, err := c.ReadMessage()
                if err != nil {
                    log.Fatal(err)
                    return
                }

                fmt.Printf("%d %d %s\n", msg.ID, msg.Channel, msg.Data)

                s.close()
            }
        }()

        _, err = c.Send(binproto.NewMessage(42, 3, []byte("hi")))
        if err != nil {
            log.Fatal(err)
        }
    })

    if err := s.serve("tcp", ":4242"); err != nil {
        log.Fatal(err)
    }

}

type server struct {
    listener net.Listener
}

func (s *server) handle(conn net.Conn) {
    defer conn.Close()

    c := binproto.NewConn(conn)

    for {
        msg, err := c.ReadMessage()
        if err != nil {
            fmt.Printf("error: %v", err)
            return
        }

        fmt.Printf("%d %d %s\n", msg.ID, msg.Channel, msg.Data)

        _, err = c.Send(binproto.NewMessage(112, 5, []byte("hey")))
        if err != nil {
            log.Fatal(err)
        }
    }
}

func (s *server) serve(network, address string) error {
    l, err := net.Listen(network, address)
    if err != nil {
        return err
    }

    s.listener = l

    for {
        if s.listener == nil {
            break
        }

        c, err := l.Accept()
        if err != nil {
            continue
        }

        go s.handle(c)
    }

    return nil
}

func (s *server) close() error {
    err := s.listener.Close()
    s.listener = nil
    return err
}

Encryption

Note that, binproto doesn't implement encryption, but there should be some module somewhere which implements the open source NOISE protocol that you can use as a drop-in replacement for net.