grpc
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.
| Function | Does |
|---|---|
client(scheme, authority, max_head, max_message) -> Conn | A client connection with h2’s preface queued: send output() first |
server(max_streams, max_head, max_message) -> Conn | A 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() -> ?Event | The 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() -> ?u64 | Keepalive: a PING with 8 bytes of your choosing, the ACKs as they come back |
c.free() | Releases it |
schemeandauthoritygo into every call’s head:httpfor cleartext HTTP/2 (a server behind a proxy, or a peer that knows it speaks HTTP/2),httpsover TLS; the host and port as the server expects them.max_headlimits a response’s (or a request’s) headers as HTTP/2 counts them,max_messagethe length of one message: a bigger one ends its call withResourceExhausted.- After every
feedsendoutput(), then take every event: a window update the peer sent may have let queued messages go. - A connection error comes from
feedonce (anh2.H2Error), and every open call ends withDoneUnavailable.
Calls on the client
| Function | Does |
|---|---|
c.call(method, metadata, timeout_ns, message) -> i64 | A unary call: one message, then the end of the request |
c.open(method, metadata, timeout_ns) -> i64 | A streaming call: send messages, then close_send |
c.send(id, message) | Queues a message (either side) |
c.pending(id) -> i64 | Bytes 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 |
methodis the path,/package.Service/Method.metadataare 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-bincarry binary values: encode them with base64 yourself.timeout_nsabove 0 sendsgrpc-timeoutso 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. callandopentrap whencan_call()is false: the server’s limit of concurrent calls is reached, or the connection is closing.
The server side
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; }
}| Function | Does |
|---|---|
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-typeother thanapplication/grpc(orapplication/grpc+protoand the like) 415. respondsends the head with your metadata; asendorfinishbefore it sends it without.finishsendsgrpc-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 withfinish(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 toh2.
Events
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, theMessages, thenDone; a server seesCall, theMessages,HalfClosed, andDoneonly when the call ended without yourfinish.Headersdoes not come when the server answered with its status alone. Call.metadataholds the request’s headers without the pseudo-headers and the protocol’s own (content-type,te,user-agent,grpc-*);timeoutis the client’sgrpc-timeoutin nanoseconds, -1 without one.Doneis the last event of a call;send,finishandcancelon 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 overmax_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 happened | Status |
|---|---|
| HTTP 400 | Internal |
| HTTP 401 | Unauthenticated |
| HTTP 403 | PermissionDenied |
| HTTP 404 | Unimplemented |
| HTTP 429, 502, 503, 504 | Unavailable |
| another HTTP status | Unknown (“HTTP status N”) |
200 that is not application/grpc | Internal |
200 that ends without grpc-status | Unknown |
| RST_STREAM CANCEL | Cancelled |
| RST_STREAM REFUSED_STREAM, or above a GOAWAY’s last stream | Unavailable: not processed, may be retried |
| RST_STREAM ENHANCE_YOUR_CALM | ResourceExhausted |
| RST_STREAM INADEQUATE_SECURITY | PermissionDenied |
| another RST_STREAM code | Internal |
| a connection error, or the transport ended | Unavailable |
a message over max_message | ResourceExhausted |
| a compressed message, or one cut by the end of the stream | Internal |
A grpc-status outside 0 to 16 is Unknown.
Helpers
| Function | Does |
|---|---|
parse_timeout(v) -> ?i64 | A grpc-timeout ("1H", "2M", "3S", "4m", "5u", "6n") in nanoseconds; null when malformed |
format_timeout(ns) -> string | Nanoseconds as a grpc-timeout, in the largest unit that keeps them exact in 8 digits (rounded up when none does) |
encode_message(text) -> string | Percent-encodes a grpc-message |
decode_message(text) -> string | Decodes one, leaving a malformed % as it is |
status_of(http_status) -> Status | The 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: