Binary protocol

The calls of the HTTP API - set, get, history, the page of a key - over one kept TLS connection. For agents and devices that write often: no HTTP headers, no new connection for a request, many requests on the way at once.

When to use it

  • HTTP - for scripts, cron jobs, a browser, curl: nothing to write.
  • Binary - for an agent that runs all the time: a value is a frame of a few dozen bytes on an open connection.

The keys, the channels, the items and their limits are the same: see Keys and Items. A value written by one protocol is read by the other.

Connection

Addressu00.io:3200
TLS1.3 only; the certificate of u00.io: check it as usual
ALPNu00/1 (optional; another protocol is refused)
Byte orderlittle endian
  • Keep the connection: send a ping when there is nothing else to send for 60 seconds. A connection with no frames for 120 seconds is closed.
  • A broken connection: connect again after a pause (1 s, then longer). The requests that had no answer may be sent again: a set sent twice writes the value twice.
  • One connection is enough for an agent: it carries any number of requests.

Frames

A request and a response are frames:

frame    = u32 length | u8 kind | u64 id | payload
           length - the bytes after it: 9 + the length of the payload

str16    = u16 length | bytes (UTF-8)
response = kind 0x80, the id of the request, payload = u8 status | body
  • id - any number of the client: the response has the same one.
  • Send requests without waiting for the responses. Ping and set are answered in the order they come; get and history - as soon as they are ready, so match the responses by id.
  • A frame of the client is up to 64 KB (length ≤ 65536). A longer one, or a length under 9, closes the connection. Many values that do not fit: send them in several sets.
  • A response may be longer: up to 64 MB (the history of an hour of a busy item is hundreds of KB). Read the length of a response as it is.

Calls

KindCallPayloadBody of the response (OK)HTTP
1ping---
2setstr16 key u16 count (str16 name str16 value)…-/set
3getstr16 channel idgzip of the JSON/get
4historystr16 item path i64 dtgzip of the JSON/history
5view urlstr16 keythe URL, text/view_url_by_api_key

set

Sets the items of the channel of the key: name → value. The answer comes at once: the values are stored in the background. The time of the values is the time the server got the frame.

  • The key is 32 hex characters; up to 1000 items; a name is up to 64 bytes, a value up to 10 KB. Over them the answer is bad request and nothing of the frame is written.
  • A channel has up to 1000 items at a time: a new item over them is dropped by the storage, without an error (as with HTTP).
  • A name written twice in a frame keeps the last value.
  • Status 3: the values are not written (the storage is down or overloaded). Keep them and send them again later: an agent keeps a small queue for this.

get

The current values of the channel: gzip (RFC 1952) of the JSON of /get, always - zlib and any gzip library unpack it. not found - no such channel.

history

  • item path - <channel id> for the item /, <channel id>/room1/humidity for the item /room1/humidity.
  • dt - unix milliseconds, any time of the hour: the block of the hour that has it (rounded down to the start of the hour). 0 - the current hour.
  • The answer is gzip of the JSON of /history; an hour with no values is an empty history.

Statuses

StatusWhat to do
0OK
1not foundno such channel
2bad request; the body is the error, textfix the request: sending it again gives the same
3error: the storage does not answer or does not keep up now; a set is not writtensend again later
4too many requestsslow down: the request is not done

An unknown kind is answered with bad request: the connection goes on.

Limits

The rate limits are the ones of the HTTP API, by the address of the client, and shared with it: the requests of both protocols and of all the connections of an address count together.

CallsPer secondBurst
set1020
get50100
history4080
ping, view url50100
ConnectionsLimit
open connections of an address256
new connections of an address20 per second, burst 100
get and history waiting for the storage, of a connection16; the next request waits
the TLS handshake10 seconds
the rest of a frame after its header15 seconds
no frames120 seconds
reading the responsesa client that does not read them for 10 seconds is disconnected
  • Over the rate limits a request is answered with status 4 and the connection goes on.
  • Over the limits of the connections a new connection is closed before the handshake.
  • A flood of frames far over the limits closes the connections of the address.
  • IPv6: the limits of an address are the ones of its /64 network.
BatchingThe limit is on requests, not on items: put the values of a moment in one set.

Example: Python

import socket, ssl, struct, itertools

PING, SET, GET, HISTORY, VIEW_URL = 1, 2, 3, 4, 5

def str16(s):
    b = s.encode()
    return struct.pack("<H", len(b)) + b

class U00:
    def __init__(self, host="u00.io", port=3200):
        ctx = ssl.create_default_context()
        ctx.minimum_version = ssl.TLSVersion.TLSv1_3
        ctx.set_alpn_protocols(["u00/1"])
        sock = socket.create_connection((host, port), timeout=30)
        self.s = ctx.wrap_socket(sock, server_hostname=host)
        self.ids = itertools.count(1)

    def _read(self, n):
        buf = b""
        while len(buf) < n:
            chunk = self.s.recv(n - len(buf))
            if not chunk:
                raise ConnectionError("closed")
            buf += chunk
        return buf

    # call sends a request and waits for its response (one at a time: the ids match)
    def call(self, kind, payload=b""):
        self.s.sendall(struct.pack("<IBQ", 9 + len(payload), kind, next(self.ids)) + payload)
        size, _, _ = struct.unpack("<IBQ", self._read(13))
        body = self._read(size - 9)
        return body[0], body[1:]

    def set(self, key, items):
        payload = str16(key) + struct.pack("<H", len(items))
        for name, value in items.items():
            payload += str16(name) + str16(str(value))
        return self.call(SET, payload)[0]

u = U00()
print(u.set("KEY", {"/": 21.5, "/_name": "Temperature"}))  # 0 - OK
print(u.call(VIEW_URL, str16("KEY"))[1].decode())

Example: Go

package main

import (
	"crypto/tls"
	"encoding/binary"
	"fmt"
	"io"
)

func str16(b []byte, s string) []byte {
	b = binary.LittleEndian.AppendUint16(b, uint16(len(s)))
	return append(b, s...)
}

func main() {
	conn, err := tls.Dial("tcp", "u00.io:3200", &tls.Config{MinVersion: tls.VersionTLS13, NextProtos: []string{"u00/1"}})
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// set: the key, the count, the items
	payload := str16(nil, "KEY")
	payload = binary.LittleEndian.AppendUint16(payload, 2)
	payload = str16(str16(payload, "/"), "21.5")
	payload = str16(str16(payload, "/_name"), "Temperature")

	frame := binary.LittleEndian.AppendUint32(nil, uint32(9+len(payload)))
	frame = append(frame, 2) // set
	frame = binary.LittleEndian.AppendUint64(frame, 1)
	conn.Write(append(frame, payload...))

	// The response: the header, then status | body
	var h [13]byte
	if _, err := io.ReadFull(conn, h[:]); err != nil {
		panic(err)
	}
	body := make([]byte, binary.LittleEndian.Uint32(h[:4])-9)
	io.ReadFull(conn, body)
	fmt.Println("id", binary.LittleEndian.Uint64(h[5:]), "status", body[0])
}

An agent that sends a lot writes the frames from one goroutine (or thread) and reads the responses in another, matching them by id.