grpc

tape
import grpc from "grpc";
import hpack from "hpack";

var c: grpc.Conn = grpc.client("http", "control.internal:50051", 65536, 4 << 20);
defer c.free();
let none: [0]hpack.Header = [];
let id: i64 = c.call("/agent.Control/Register", none[..], 5000000000, request_bytes);
send(c.output());                     // your transport: TCP with prior knowledge, or TLS that chose "h2"
c.advance_output(c.output().len);
var done: bool = false;
while (!done) {
    let n: i64 = receive(buf[..]);    // what the server sent
    if (n == 0) { c.end_of_input(); } else { c.feed(buf[0..n]) or {}; }
    send(c.output());                 // acknowledgements, window updates, queued messages
    c.advance_output(c.output().len);
    while (true) {
        let e: ?grpc.Event = c.next_event();
        if (e == null) { break; }
        match (e!) {
            grpc.Event.Message { id; data } => { /* the reply: decode it with protobuf */ }
            grpc.Event.Done { id; status; message; trailers } => {
                if (status != grpc.Status.Ok) { io.println("{status.desc()}: {message}"); }
                done = true;
            }
            _ => {}
        }
    }
}

grpc is gRPC over HTTP/2 for a client and for a server, on top of h2 and, like it, without the I/O: you feed it what the peer sent, send what it queues, and take what happened as events. It does the message framing, the headers the protocol asks for, the status in the trailers, and gives every call a status even when the server never sent one. The messages are bytes: encode and decode them with protobuf (or anything else both sides agree on). Choosing the method, deadlines, retries and the transport are yours.

FunctionDoes
client(scheme, authority, max_head, max_message) -> ConnA client connection with h2’s preface queued: send output() first
server(max_streams, max_head, max_message) -> ConnA server connection with h2’s SETTINGS queued: send them first
c.feed(data)What the peer sent: every whole frame is processed, events queued
c.output(), c.advance_output(n)What to send, and how much was sent
c.next_event() -> ?EventThe next thing that happened, in order
c.end_of_input()The transport ended: every open call ends
c.can_call(), c.active(), c.finished(), c.error()Whether a call can start now; calls open; whether none ever can again; the connection’s error
c.goodbye()GOAWAY: no new calls, before you close the transport
c.ping(data), c.next_pong() -> ?u64Keepalive: a PING with 8 bytes of your choosing, the ACKs as they come back
c.free()Releases it
  • scheme and authority go into every call’s head: http for cleartext HTTP/2 (a server behind a proxy, or a peer that knows it speaks HTTP/2), https over TLS; the host and port as the server expects them.
  • max_head limits a response’s (or a request’s) headers as HTTP/2 counts them, max_message the length of one message: a bigger one ends its call with ResourceExhausted.
  • After every feed send output(), then take every event: a window update the peer sent may have let queued messages go.
  • A connection error comes from feed once (an h2.H2Error), and every open call ends with Done Unavailable.

Calls on the client

FunctionDoes
c.call(method, metadata, timeout_ns, message) -> i64A unary call: one message, then the end of the request
c.open(method, metadata, timeout_ns) -> i64A streaming call: send messages, then close_send
c.send(id, message)Queues a message (either side)
c.pending(id) -> i64Bytes queued and not yet on the wire (either side)
c.close_send(id)No more messages: the end of the request after the queue
c.cancel(id)Cancels the call (either side): nothing more is heard of it
  • method is the path, /package.Service/Method. metadata are your headers with lowercase names; the protocol’s own (content-type, te, grpc-*) and HTTP/1.1’s connection headers are not metadata (giving one traps). Names ending in -bin carry binary values: encode them with base64 yourself.
  • timeout_ns above 0 sends grpc-timeout so the server knows your deadline; enforcing it is yours (cancel the call when it passes).
  • Messages go out as the server’s flow-control windows allow. To stream a large amount, send a message, then wait until pending(id) falls before sending more.
  • call and open trap when can_call() is false: the server’s limit of concurrent calls is reached, or the connection is closing.

The server side

tape
var c: grpc.Conn = grpc.server(100, 65536, 4 << 20);
defer c.free();
send(c.output());
c.advance_output(c.output().len);
while (true) {
    let n: i64 = receive(buf[..]);
    if (n == 0) { c.end_of_input(); } else { c.feed(buf[0..n]) or {}; }
    while (true) {
        let e: ?grpc.Event = c.next_event();
        if (e == null) { break; }
        match (e!) {
            grpc.Event.Call { id; method; metadata; timeout } => {
                if (!str.eq(method, "/agent.Control/Register")) {
                    let none: [0]hpack.Header = [];
                    c.finish(id, grpc.Status.Unimplemented, "unknown method", none[..]);
                }
            }
            grpc.Event.Message { id; data } => {
                let none: [0]hpack.Header = [];
                c.send(id, answer(data));
                c.finish(id, grpc.Status.Ok, "", none[..]);
            }
            grpc.Event.HalfClosed { id } => { /* the client sent its last message */ }
            grpc.Event.Done { id; status; message; trailers } => { /* cancelled, or the connection went */ }
            _ => {}
        }
    }
    send(c.output());
    c.advance_output(c.output().len);
    if (n == 0 || (c.finished() && c.active() == 0)) { break; }
}
FunctionDoes
c.respond(id, metadata)The response’s initial metadata (optional)
c.send(id, message)Queues a message
c.finish(id, status, message, trailers)Ends the call with its status, after the queued messages
c.cancel(id)Resets the call
  • A request that is not gRPC never reaches you: a method other than POST is answered 405, a content-type other than application/grpc (or application/grpc+proto and the like) 415.
  • respond sends the head with your metadata; a send or finish before it sends it without. finish sends grpc-status, grpc-message (when the message is not empty, percent-encoded) and your trailers; when nothing was sent before, the whole answer is one head (trailers-only). Answer a method you don’t have with finish(id, Unimplemented, ...).
  • A call you finish before the client sent its last message is reset after the trailers, so the client stops sending.
  • active() counts a call until its trailers went to h2.

Events

tape
pub tagged Event {
    Call { id: i64; method: []const u8; metadata: *const hpack.Vec_Header; timeout: i64; }   // server: a new call
    Headers { id: i64; metadata: *const hpack.Vec_Header; }      // client: the server's initial metadata
    Message { id: i64; data: []const u8; }                       // either: one whole message
    HalfClosed { id: i64; }                                      // server: the client sent its last message
    Done { id: i64; status: Status; message: []const u8; trailers: *const hpack.Vec_Header; }   // the call is over
}
  • An event’s views are valid until the next next_event: copy what you keep.
  • A client sees Headers, the Messages, then Done; a server sees Call, the Messages, HalfClosed, and Done only when the call ended without your finish. Headers does not come when the server answered with its status alone.
  • Call.metadata holds the request’s headers without the pseudo-headers and the protocol’s own (content-type, te, user-agent, grpc-*); timeout is the client’s grpc-timeout in nanoseconds, -1 without one.
  • Done is the last event of a call; send, finish and cancel on it afterwards do nothing. On a client: the server’s status, its message decoded, and its trailing metadata. On a server: Cancelled (the client cancelled), Unavailable (the connection failed or ended), or the status the engine answered itself (a message over max_message, a compressed or cut message).

Statuses

Status holds the 17 codes, Ok (0) to Unauthenticated (16); status.desc() is the protocol’s name ("UNAVAILABLE"). When a call ends without a grpc-status, the client makes one:

What happenedStatus
HTTP 400Internal
HTTP 401Unauthenticated
HTTP 403PermissionDenied
HTTP 404Unimplemented
HTTP 429, 502, 503, 504Unavailable
another HTTP statusUnknown (“HTTP status N”)
200 that is not application/grpcInternal
200 that ends without grpc-statusUnknown
RST_STREAM CANCELCancelled
RST_STREAM REFUSED_STREAM, or above a GOAWAY’s last streamUnavailable: not processed, may be retried
RST_STREAM ENHANCE_YOUR_CALMResourceExhausted
RST_STREAM INADEQUATE_SECURITYPermissionDenied
another RST_STREAM codeInternal
a connection error, or the transport endedUnavailable
a message over max_messageResourceExhausted
a compressed message, or one cut by the end of the streamInternal

A grpc-status outside 0 to 16 is Unknown.

Helpers

FunctionDoes
parse_timeout(v) -> ?i64A grpc-timeout ("1H", "2M", "3S", "4m", "5u", "6n") in nanoseconds; null when malformed
format_timeout(ns) -> stringNanoseconds as a grpc-timeout, in the largest unit that keeps them exact in 8 digits (rounded up when none does)
encode_message(text) -> stringPercent-encodes a grpc-message
decode_message(text) -> stringDecodes one, leaving a malformed % as it is
status_of(http_status) -> StatusThe status the protocol gives an HTTP status

Not covered: compression (grpc-encoding other than identity: a compressed message ends its call), deadlines enforced by the engine, retries, load balancing, gRPC-Web, server reflection and health checking (an ordinary call like any other).

Last modified: