Connections
Every protocol runs on a
connection unless it
picks the lockstep datagram framing, in which case it gets a
client and server instead. This page uses this
schema:
protocol Chat {
function send(text: string) -> u64;
function count() -> u64;
}
@provider = Client
protocol Events {
function message(text: string) -> bool;
function ping();
}
Serving every protocol
The package gets one serve, with a Services struct holding a field per
protocol the server provides. Leaving one out doesn't compile:
use my_package::{connect, serve, ClientServices, Services};
use my_package::chat::ChatSender;
// The side that accepts: handshake, provide every protocol, serve until
// the peer leaves.
serve(transport, Services { chat: ChatImpl })?;
// The side that dials: handshake, provide the client's protocols, and call
// the server's through their senders.
let mut conn = connect(transport, ClientServices { events: EventsImpl::default() })?;
let sent = ChatSender::new(&mut conn).send("hi")?;
accept(transport, Services { .. })isservewithout the serving: it returns the connection, for a server that calls the client first or serves one request at a time.ClientServicesholds the protocols the dialling side provides (@provider = ClientorMultiple). With none,connecttakes only the transport.- Field names are the protocol's name in snake case
(
CharacterInventoryischaracter_inventory). Where two schemas declare a protocol of the same name, both are prefixed with their namespace. - The format is the package's
Format(Framing & wire format), and the framing and handshake are the protocols'. When the protocols on connections disagree on either, no one connection carries them all, so these functions aren't generated andcomline generatewarns why.
In lib mode they're at the crate root; in code mode, in connection.rs
beside the schema files.
What codegen emits
pub trait Chat {
fn send(&self, peer: &mut impl Peer, text: &str) -> Result<u64, Fault<ChatSendError>>;
fn count(&self, peer: &mut impl Peer) -> Result<u64, Fault<ChatCountError>>;
}
pub struct ChatDispatcher<S>(pub S); // PROTOCOL, provide_on(&mut Connection)
pub struct ChatSender<'p, P: Peer>(pub &'p mut P);
pub trait Events { /* the same shape */ }
pub struct EventsDispatcher<S>(pub S);
pub struct EventsSender<'p, P: Peer>(pub &'p mut P);
// Once per package:
pub type Format = comline_runtime::format::Packed;
pub const CONNECTION_IR_HASH: u64 = 0x…; // checked by the handshake
pub struct Services<Chat> { pub chat: Chat }
pub struct ClientServices<Events> { pub events: Events }
pub fn accept(…); pub fn serve(…); pub fn connect(…);
It differs from a client and server in three ways:
- Handlers take the peer first: the other end of the connection the call
came in on. Through it a handler calls back (
EventsSender::new(peer)), readspeer.id(), and keeps data on the connection withpeer.extensions(). - A sender replaces the client.
ChatSenderhas one method per function, asChatClientwould, over anything that implementsPeer: aConnection, or a handler's peer. Calls returnCallErroras a client's do;@timeout_msapplies; one-way functions returnResult<(), RuntimeError>. CONNECTION_IR_HASHcovers every schema in the package. It also fixes each protocol's number (ChatDispatcher::PROTOCOL), its position among the package's protocols.
Protocols on datagram in the same package keep their Client and
Server, and so do jsonrpc ones unless the package has a JSON-RPC protocol
on a connection (see Over JSON-RPC).
By hand
serve is a few lines over the runtime's Connection, which is also how to
pick another format for one connection:
use comline_runtime::connection::Connection;
let mut conn = Connection::accept(transport, Format::default(), CONNECTION_IR_HASH)?;
ChatDispatcher(ChatImpl).provide_on(&mut conn);
conn.serve()?; // until the transport closes
serve_one() handles a single frame instead, returning Ok(false) once the
transport has closed. Between frames, this side can call the client on its
own:
The dialling side is Connection::connect with the same arguments. While a
call waits for its reply, the connection serves whatever the server sends
first, such as the Events::message call its handler makes. A request to a
protocol this side doesn't provide is answered with a fault.
Over JSON-RPC
A JSON-RPC protocol the client provides runs on a connection too, and then so
do the package's other JSON-RPC protocols
(the rules). Open the
connection with JsonRpcFraming, which the package's serve and connect
do for you; the rest of this page is unchanged:
use comline_runtime::framing::JsonRpcFraming;
use comline_runtime::handshake::ComlineHandshake;
let mut conn = Connection::accept_with(transport, Json, JsonRpcFraming, &mut ComlineHandshake, CONNECTION_IR_HASH)?;
// The dialling side:
let mut conn = Connection::connect_with(transport, Json, JsonRpcFraming, &mut ComlineHandshake, CONNECTION_IR_HASH)?;
An AsyncConnection opens with
AsyncConnection::handshake(transport, Json, JsonRpcFraming, CONNECTION_IR_HASH).
- Each call table holds method names:
["send", "count"], or undermethod_names = qualified,["chat.Events/message", "chat.Events/ping"]. A request is routed to the provided protocol whose table has its method. - A one-way call is a notification, sent with no
id. A request without one gets no reply, even if its function returns a value.
A handler calling back
struct Sent(u64);
impl Chat for ChatImpl {
fn send(&self, peer: &mut impl Peer, text: &str) -> Result<u64, Fault<ChatSendError>> {
let shown = EventsSender::new(peer).message(text).map_err(|_| Fault::Internal)?;
let sent = peer.extensions().get_mut::<Sent>().map(|s| { s.0 += 1; s.0 });
let sent = sent.unwrap_or_else(|| { peer.extensions().insert(Sent(1)); 1 });
Ok(sent)
}
// …
}
Extensions holds one value per type (insert, get, get_mut,
remove). It belongs to the connection, so Sent counts this client's
messages and no one else's.
Threads
A Connection is blocking and does one thing at a time. A request is
handled to the end before the next frame is read, and a call made while
serving finishes before the request that made it. To call the peer from
another task while the connection serves, generate for the async driver.
Async
With async = true in comline.toml,
connection protocols are generated for the runtime's AsyncConnection
instead, on any executor:
pub trait Chat: Send + Sync + 'static {
fn send(&self, peer: &impl AsyncPeer, text: &str)
-> impl Future<Output = Result<u64, Fault<ChatSendError>>> + Send;
// …
}
pub struct ChatAsyncDispatcher<S>(pub S); // PROTOCOL, provide_on(&mut AsyncConnection)
pub struct ChatAsyncSender<'p, P: AsyncPeer>(pub &'p P);
A handler is written as an async fn, and awaits its calls back:
impl Chat for ChatImpl {
async fn send(&self, peer: &impl AsyncPeer, text: &str) -> Result<u64, Fault<ChatSendError>> {
let shown = EventsAsyncSender::new(peer).message(text).await.map_err(|_| Fault::Internal)?;
// …
}
}
The connection is a future the application spawns; its handles call the
peer from any task. The generated crate enables the runtime's async
feature; in code mode, the file names the feature to add.
The package's serve, accept and connect are async too. accept and
connect return the connection not yet running, so the caller can take a
handle() before spawning it:
let server = tokio::spawn(serve(acceptor, Services { chat: ChatImpl }));
let client = connect(dialler, ClientServices { events: EventsImpl::default() }).await?;
let to_server = client.handle();
tokio::spawn(client.run());
let sent = ChatAsyncSender::new(&to_server).send("hi").await?;
Requests are handled in order, except that while every running handler
waits on a call to the peer, the next one starts: it may be that call's
nested request. conn.concurrent(n) lets several run at once, and
conn.set_timer(…) gives @timeout_ms a clock. The runtime's tokio
feature adds TCP (async_connection::tokio::Tcp) and a timer.
Protocols that aren't on a connection keep their blocking Client and
Server either way.
Without generated code
Connection::provide(protocol, dispatcher) registers any Dispatch. One
that needs the peer implements Dispatch::dispatch_with, whose default
calls dispatch. Connection implements Peer, whose call, notify and
call_with_timeout take the protocol number. Underneath, connection::State
is the I/O-free core: it encodes requests and replies, and turns frames
into Events (a request, a reply, or a stale reply to an abandoned call).