Networking for MoonBit: connections, listeners and dialers over TCP, TLS and QUIC.
// One request, whichever of the four carries it.
let client = @tls.Client::new(@roots.mozilla(), alpn=["h2"])
let conn = @h2.connect("example.com", port=443, tls=client)
let (response, body) = @spec.call(conn, @spec.Request::new("GET", "/"))
println("\{response.status} \{body.length()}")
conn.close()moon add moonbitstack/moonnet| Declaration | Shape |
|---|---|
| Field | { name, value }, one field of a head |
| Body | Empty, Bytes(Bytes), Streaming |
| Request | { verb, target, fields, body } |
| Response | { status : Int32, fields } — the head; the body is read through the connection |
| Stream | one exchange, numbered by the connection rather than by the protocol under it |
| Refused | Malformed, Unsupported, Closed, Broken |
pub(open) trait Conn {
async fn open(self : Self, request : Request) -> Stream
async fn send(self : Self, stream : Stream, bytes : Bytes, end~ : Bool) -> Unit
async fn response(self : Self, stream : Stream) -> Response
async fn read(self : Self, stream : Stream) -> Bytes?
fn trailers(self : Self, stream : Stream) -> Array[Field]
fn is_ready(self : Self) -> Bool
fn kind(self : Self) -> String
async fn close(self : Self) -> Unit
}async fn fetch(conn : &@spec.Conn, path : String) -> Bytes {
let stream = conn.open(@spec.Request::new("GET", path))
let response = conn.response(stream)
assert_eq(response.status, 200)
@spec.read_to_end(conn, stream)
}| Package | Protocol | Public entry points |
|---|---|---|
| adapt/h1 | HTTP/1.1, RFC 9112 | client(tcp_conn, authority), connect(host, port~, tls?) |
| adapt/h2 | HTTP/2, RFC 9113 | client(tcp_conn, scheme~, authority~), connect(host, port~, tls?) |
| adapt/h3 | HTTP/3, RFC 9114 | connect(host, port~, tls~) |
| adapt/ws | WebSocket, RFC 6455 | client(tcp_conn, host~), connect(url, tls?) |
// A session, and the reply to what the client said.
async fn hello(url : String, tls : @tls.Client) -> Unit {
let ws = @ws.connect(url, tls=tls)
let stream = ws.open(@spec.Request::new("GET", "/chat"))
let _ = ws.response(stream)
ws.send(stream, @utf8.encode("hello"), end=false)
match ws.read(stream) {
Some(reply) => println(@utf8.decode_lossy(reply[:]))
None => println("no reply")
}
ws.close()
}async fn pooled(tls : @tls.Client) -> Unit {
let pool = @pool.Pool::new(
key => {
let sock = @tcp.dial(key.host, port=key.port, tls=tls)
@pool.Opened::new(sock, @h2.client(sock, scheme="https", authority=key.host))
},
config={ ..@pool.defaults, idle: 60000, max_total: 128 },
)
let key = @pool.Key::new("example.com", 443, alpn="h2")
assert_eq(key.alpn, "h2")
// The same `fetch` the adapters answer, on a connection the pool chose.
let conn = pool.get(key)
let body = fetch(conn, "/")
println(body.length())
conn.close()
pool.close()
}| Field | Preset | What it is |
|---|---|---|
| idle | 30000 ms | how long a connection may sit idle before it is closed instead of reused |
| max_idle | 8 | how many idle connections one key keeps |
| max_total | 64 | how many the pool holds open at once, in use or idle |
| liveness | Peek | what a reused connection is asked first: nothing, or a read that does not wait |
| wait | 5000 ms | how long a caller waits for a slot when the pool is full |
async fn datagrams() -> Unit {
let socket = @udp.bind(port=12000)
let to = socket.addr()
socket.send(to, b"ping")
match socket.recv(timeout=100) {
Some((bytes, _)) => println("\{bytes.length()} octets")
None => println("nothing came back")
}
socket.close()
}
async fn quic_session(tls : @tls.Client) -> Unit {
let quic = @udp.dial("example.com", port=443, tls=tls)
let stream = quic.open_bi()
quic.write(stream, b"hello", fin=true)
quic.close()
}| Conn::plain(sock) | a socket in the clear |
| Conn::server(sock, config) | the server side of TLS, returned after the handshake |
| Conn::client(sock, config, host) | the client side, returned after the server's chain is judged |
| starts_with(prefix) | whether the stream opens with prefix, reading only as far as the first byte that differs |
| opens_tls(), secure(config) | serve TLS and the clear on one port |
| is_alive(wait~) | whether the peer is still there, asked without waiting |
| shutdown(), close() | a close_notify, then the socket |
@tcp.serve((conn, _) => {
while conn.read_until("\n") is Some(line) {
conn.write(line + "\n")
}
}, port=12000)| Directory | What it does |
|---|---|
| src/examples/tour | a TLS server and a client talking over it in one run |
| src/examples/01-interop | one client that speaks all four protocols through the contract, for driving against another server |
| src/examples/02-plug | a connection implemented outside this library, run through the same helpers |
| src/examples/03-front-door | the front door on its own: a listener and a client with one import |
| src/examples/04-ws-conformance | the client end of a WebSocket conformance run, for a fuzzing server to drive |
| src/examples/tls-peer | a TLS peer for interop runs against OpenSSL and tlsfuzzer |
moon run src/examples/tour --target native| What | Against | Result |
|---|---|---|
| HTTP/1.1 — a head, a declared-length body, a chunked body with trailers, a body to the close | two independent servers | all exchanges answered, bodies byte for byte |
| HTTP/2 — preface, settings, one exchange per stream, flow control both ways | the same two | all exchanges answered; a reply over one window arrives whole |
| HTTP/3 — QUIC handshake, settings with the QPACK table shut, one stream per exchange | both, where the server offers h3 | all exchanges answered |
| WebSocket — the upgrade, framing, ping and pong, close, invalid UTF-8, reserved bits | Autobahn's fuzzing server | 244 graded cases pass, 4 of them non-strict; no failures |
| TLS 1.3 over TCP — handshake, certificate chain, alerts | OpenSSL, and tlsfuzzer | OpenSSL interop passes; tlsfuzzer runs in CI |
let opened = @moonnet.connect("https://example.com/", alpn=["h2", "http/1.1"])
if opened.alpn == "h2" {
let conn = @h2.client(opened.conn, scheme="https", authority="example.com")
let (response, body) = @spec.call(conn, @spec.Request::new("GET", opened.target))
println("\{response.status} \{body.length()}")
}Install
Download zipNetworking for MoonBit: connections, listeners and dialers over TCP, TLS and QUIC.