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
| Address | u00.io:3200 |
| TLS | 1.3 only; the certificate of u00.io: check it as usual |
| ALPN | u00/1 (optional; another protocol is refused) |
| Byte order | little endian |
- Keep the connection: send a
pingwhen 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
| Kind | Call | Payload | Body of the response (OK) | HTTP |
|---|---|---|---|---|
1 | ping | - | - | - |
2 | set | str16 key u16 count (str16 name str16 value)… | - | /set |
3 | get | str16 channel id | gzip of the JSON | /get |
4 | history | str16 item path i64 dt | gzip of the JSON | /history |
5 | view url | str16 key | the 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 requestand 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/humidityfor 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
| Status | What to do | |
|---|---|---|
0 | OK | |
1 | not found | no such channel |
2 | bad request; the body is the error, text | fix the request: sending it again gives the same |
3 | error: the storage does not answer or does not keep up now; a set is not written | send again later |
4 | too many requests | slow 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.
| Calls | Per second | Burst |
|---|---|---|
| set | 10 | 20 |
| get | 50 | 100 |
| history | 40 | 80 |
| ping, view url | 50 | 100 |
| Connections | Limit |
|---|---|
| open connections of an address | 256 |
| new connections of an address | 20 per second, burst 100 |
| get and history waiting for the storage, of a connection | 16; the next request waits |
| the TLS handshake | 10 seconds |
| the rest of a frame after its header | 15 seconds |
| no frames | 120 seconds |
| reading the responses | a client that does not read them for 10 seconds is disconnected |
- Over the rate limits a request is answered with status
4and 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.
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.