Networking for MoonBit: connections, listeners and dialers over TCP, TLS and QUIC.
// One request, whichever of the four carries it.
let conn = @h2.connect("example.com", port=443, tls=@tls.Client::new(@roots.mozilla(), alpn=["h2"]))
let (response, body) = @spec.call(conn, @spec.Request::new("GET", "/"))
println("\{response.status} \{body.length()}")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 get(conn : &@spec.Conn, path : String) -> String {
let stream = conn.open(@spec.Request::new("GET", path))
let response = conn.response(stream)
let body = @spec.read_to_end(conn, stream)
"\{response.status} \{body.length()}"
}| 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 close the peer gave.
let ws = @ws.connect("wss://example.com/chat", tls=client)
let stream = ws.open(@spec.Request::new("GET", "/chat"))
let _ = ws.response(stream)
ws.send_text("hello")
match ws.next() {
Some(@frame.Message::Text(reply)) => println(reply)
Some(@frame.Message::Close(status~, ..)) => println("closed \{status}")
_ => ()
}let pool = @pool.Pool::new(
key => @tcp.dial(key.host, port=key.port, tls=tls_for(key)),
config={ ..@pool.defaults, idle: 60000, max_total: 128 },
)
let key = @pool.Key::new("example.com", 443, alpn="h2")
let conn = pool.get(key)
// ... one connection's worth of work ...
pool.put(key, conn)| 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 |
let socket = @udp.bind(port=12000)
socket.send(to, datagram)
let next = socket.recv(timeout=100)
let quic = @udp.dial("example.com", port=443, tls=client)
let stream = quic.open_bi()
quic.write(stream, b"hello", fin=true)| 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, peer) => {
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.