MoonCat

MoonBit 的 ASGI 3.0 服务器。接住连接,把请求拆成 scope、receive、send 交给应用。HTTP/1.1、HTTP/2、HTTP/3 与 WebSocket 同端口。

$ moon add moonbitstack/mooncat
mooncat 的标志:一轮新月前,一只猫趴在两条服务器横杆上张望。

它做什么一张图看完全部工作

mooncat 管服务器里与协议无关的那一半:套接字、连接、分段到达的请求、分段离开的响应。应用拿到 scope,以及读请求体、写响应的两个函数。路由到模板都属于应用。

客户端连到 mooncat,mooncat 把请求以 scope、receive、send 的形式交给应用。 客户端 curl、浏览器 mooncat 接受循环 · 每条连接一个任务 h1.1 h2 h3 ws tls 端口、限额、keep-alive、lifespan、停机 应用拿到的 scope、receive 与 send 你的应用 路由、JSON、模板 TCP · UDP scope receive() 读请求体,send() 写响应 应用看不到套接字,mooncat 看不到路由。
一条连接一个任务。应用拿到的是 scope,请求体边到边读。

一个端口,每一代协议

选择发生在连接打开时:TLS 在握手定,明文看头几个字节。最后都落到同一处。

线上协议由什么决定应用看到什么
HTTP/1.1明文里的请求行一个 http scope;请求体一段一段到达
HTTPSTLS 握手,ALPN 里给出 h2 与 http/1.1同一个 scope,scheme 为 https,证书在 extensions 里
h2c连接前言 PRI * HTTP/2.0同一个 scope,每条流一个
HTTP/2ALPN 应答 h2每条流一个 scope,每条流一个任务
HTTP/3UDP 上的同一个端口号每条流一个 scope,由 alt-svc 告知
ws · wss开头的 Upgrade: websocket一个 websocket scope:connect、accept、receive、send、close

快速开始

shell
moon add moonbitstack/mooncat
moon add moonbitstack/moonasgi

应用是接受三个参数的异步函数,用两个事件作答:

src/main.mbt
/// 谁连上来就问候谁的服务器。
async fn main {
  let app : @moonasgi.AsgiApp = (_scope, _receive, send) => {
    send(
      @moonasgi.Event::HttpResponseStart(
        status=200,
        headers=[("content-type", "text/plain; charset=utf-8")],
        trailers=false,
      ),
    )
    send(@moonasgi.Event::HttpResponseBody(body=b"hello from mooncat\n", more_body=false))
  }
  @mooncat.serve(app, host="127.0.0.1", port=12000)
}
shell
moon run src/main --target native
curl -i http://127.0.0.1:12000/
recorded — 对着上面的服务器执行 curl
$ curl --noproxy '*' -i http://127.0.0.1:12000/
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
server: mooncat
date: Mon, 02 Nov 2026 10:11:23 GMT
content-length: 19

hello from mooncat

一次实录moon run src/examples/09-tour

tour 示例起真实服务器,用真实客户端打,打印结果后停下。以下均为实录。

moon run src/examples/09-tour --target native
$ moon run src/examples/09-tour --target native
# configuration, as the command line reads it
  bind                 127.0.0.1:12001
  keep-alive           30000 ms
  retire after         100
  trusted proxies      127.0.0.1, 10.0.0.0
  tls                  false

# lifespan: startup and shutdown bracket the listener
  order                startup, serving, shutdown

# an app on a socket, answered by a real client
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Started server process on http://127.0.0.1:12031
INFO:     127.0.0.1:56255 - "GET /echo/moonbit HTTP/1.1" 200 OK
  GET /echo/moonbit    200 moonbit
INFO:     127.0.0.1:56256 - "GET / HTTP/1.1" 200 OK
  GET /                200 root
  server header        mooncat
  date header          true

# keep-alive, and the request limit that retires a server
  three requests       200, 200, 200
  the fourth           refused, the server retired

# a trusted proxy is believed, an untrusted one is not
  believed             https from 203.0.113.7
  ignored              http from 127.0.0.1

# websocket: connect, echo, close
  sent 'one', got      echo one
  sent 'two', got      echo two

# http/2 over cleartext
  listening h2c on     127.0.0.1:12036
  try it with          curl --http2-prior-knowledge http://127.0.0.1:12036/echo/h2

# graceful shutdown drains what is in flight
  stop asked for while it was still running
  the slow request     200 slow

各个包

只答 TCP 的程序不会链进 UDP 那一半。拆分看链接成本,不看阅读顺序。

包用来做什么
mooncat入口函数的重导出,小程序的全部就是 @mooncat.serve
mooncat/configConfig、访问与错误日志、HTTP 日期
mooncat/lifespan监听之前的 startup,之后的 shutdown
mooncat/proxy受信代理报告的客户端与 scheme
mooncat/serve接受循环、HTTP/1.1、HTTP/2、WebSocket、TLS、多 worker
mooncat/cli命令行,以及把 run(app) 当作整个程序
mooncat/quicHTTP/3:UDP 套接字、数据报循环与请求流

有异步套接字的地方就能服务:native 与 wasm,测试两边都跑。QUIC 只在 native。