Sign in

    moonfcgi

    Native FastCGI codec, streaming Responder/Authorizer/Filter and TCP/POSIX server

    fastcgi
    fcgi
    server
    protocol
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    yesterday
    Downloads
    14

    #MoonFCGI

    CI License: Apache-2.0

    MoonFCGI 是 MoonBit 原生 FastCGI 1.0 协议库与异步服务端库,为 MoonBit 程序提供与现有 FastCGI 服务器及部署环境互操作的基础组件。

    项目包含独立协议核心、缓冲式与流式应用接口、Responder/Authorizer/Filter 角色、TCP/POSIX 传输,以及服务生命周期与诊断能力。协议和会话使用 MoonBit 实现,POSIX 系统调用通过轻量原生适配接入官方异步 I/O。

    #核心能力

    • 协议编解码:增量 record 与 PARAMS 解析,支持任意分片、padding、二进制参数与正文、显式流结束及截断校验。
    • 应用接口:Request/Response/Handler 与 StreamRequest/BodyStream/ResponseWriter;流式 handler 在 PARAMS 结束后启动,按块读取和写出。
    • 协议角色:Responder、Authorizer 与 Filter;管理查询、能力报告、ABORT、KEEP_CONN、请求 ID 顺序复用及会话代次隔离。
    • 部署接入:TCP、POSIX 文件系统 Unix socket、继承 TCP/Unix 监听描述符;Nginx JSON API、二进制传输和持久上游示例。
    • 生命周期:显式优雅停机、在途响应排空、宽限期取消、连接与子任务清理。
    • 资源与诊断:可配置输入队列、连接/大小上限、分阶段期限、结构化事件、异常隔离回调和统计快照。

    #快速开始

    当前仓库接口版本为 0.2.0,通过源码工作区使用完整接口:

    git clone https://github.com/Ryan-1125/moonfcgi.git cd moonfcgi moon update moon check --target native moon build --target native moon test --target native moon run cmd/stream --target native -- 127.0.0.1:9000

    工具链验证环境:moon 0.1.20261020、moonc v0.10.16+b1a0e846e;官方异步依赖为 moonbitlang/async 0.20.0。服务端使用 native 后端,Windows x64 与 Ubuntu 24.04 均有实际运行验证。

    分发入口版本接口范围
    本仓库 / 本地打包归档0.2.0缓冲与流式 API、三个角色、POSIX 部署、生命周期及诊断
    Mooncakes0.1.0增量协议核心、缓冲式 TCP Responder

    使用注册表 0.1.0 的独立工程执行 moon add Ryan-1125/moonfcgi@0.1.0。0.2.0 的下游工作区接入与实际归档消费示例见 安装与部署 和 消费者验证脚本。

    #流式应用示例

    根包别名为 fcgi,服务端包别名为 server;完整包配置见 cmd/stream/moon.pkg。

    async fn main {
    let handler : @server.StreamHandler = (request, response) => {
    response.start(headers=[("Content-Type", "application/octet-stream")])
    while request.body.read() is Some(chunk) {
    response.write(chunk)
    }
    }
    @server.serve_stream("127.0.0.1:9000", handler)
    }

    BodyStream.read() 以 FastCGI 终止记录表示 EOF;ResponseWriter 按 record 写出并等待 socket 背压,handler 返回后由库完成输出流和 END_REQUEST。缓冲式入口为 serve,适合在明确大小上限内处理完整正文。

    #Nginx 接入

    使用 流式配置,在仓库根目录创建 logs、temp 后启动 Nginx:

    nginx -p <仓库绝对路径>/ -c examples/nginx-stream.conf curl --data-binary @payload.bin http://127.0.0.1:8080/echo -o echoed.bin curl http://127.0.0.1:8080/download -o download.bin

    流式配置关闭请求和响应缓冲。JSON API、重复响应头、错误状态及持久上游示例见 缓冲式配置 与 cmd/main。Unix socket 权限、继承监听和进程生命周期接入见 部署指南。

    #文档与代码导航

    入口内容
    架构与目录模块职责、数据路径与源码位置
    API 与协议契约公开接口、角色、字节语义与资源约束
    流式处理STDIN/DATA、输出背压与错误完成语义
    服务生命周期Control、事件、统计、队列与期限配置
    安装与部署源码工作区、Nginx、TCP/Unix/fd 接入
    测试与兼容性真实联调结果、版本和复现命令
    参考来源规范、成熟实现及参考范围
    项目申报书项目定位、场景与验收交付

    完整公开签名见 协议核心接口 和 服务端接口。

    #验证

    Linux CI 覆盖格式、检查、构建、接口一致性、28 项单元测试、13 组协议联调、16 组流式/角色/POSIX 联调、12 组生命周期联调,以及独立包消费者。

    互操作对象包括真实 Nginx、Go 1.23.2 net/http/fcgi 和系统 libfcgi。测试覆盖请求结束前的双向传输、9 MiB 二进制上传、上游连接复用、角色处理、超时、中止、Unix/fd 接入及优雅停机。

    #协议与运行契约

    每个连接同时处理一个活动请求,不同连接并发;能力查询报告 FCGI_MPXS_CONNS=0,并发 BEGIN 以协议状态拒绝。角色通过允许列表配置,默认 Responder。Nginx 验证使用 Responder,Authorizer/Filter 使用独立协议驱动与 libfcgi 对照。

    输入队列、总字节数和连接数有明确上限;停机及取消采用协作式任务清理。POSIX 部署接口适用于 POSIX 环境,HTTP 边缘服务、TLS 和进程管理由部署环境承担。详细期限、所有权及异常语义见 API 和生命周期文档。

    #开源许可

    采用 Apache-2.0。FastCGI 规范、Go、libfcgi 与 async 的来源及许可见 第三方说明;对应许可证保存在 licenses。

    ProtocolError

    pub suberror ProtocolError {
    Invalid(String)
    Limit(String)
    Truncated(String)
    } derive(
    Debug
    )

    Decoder

    pub struct Decoder {
    // private fields
    }

    Incremental decoder. A failure poisons the decoder; create a new one.

    Decoder::feed

    fn Decoder::feed(self : Decoder, bytes : Bytes) -> Array[Record] raise ProtocolError

    Accept at most 64 KiB per call. Partial records remain buffered.

    Decoder::finish

    fn Decoder::finish(self : Decoder) -> Unit raise ProtocolError

    Call only at transport EOF. EOF cannot terminate a partial record.

    Decoder::new

    fn Decoder::new() -> Decoder

    Event

    pub(all) enum Event {
    Send(Array[Record])
    Ready(Request)
    Aborted(Int64)
    Close
    }

    Events are consumed in order by a transport adapter.

    Limits

    pub struct Limits {
    body_bytes : Int
    params_bytes : Int
    param_count : Int
    response_bytes : Int
    connections : Int
    }

    Configuration is immutable after validation.

    Limits::new

    fn Limits::new(body_bytes? : Int, params_bytes? : Int, param_count? : Int, response_bytes? : Int, connections? : Int) -> Limits raise ProtocolError

    ParamsDecoder

    pub struct ParamsDecoder {
    // private fields
    }

    Incremental byte-state decoder. No allocation uses a declared remote length.

    ParamsDecoder::feed

    fn ParamsDecoder::feed(self : ParamsDecoder, data : Bytes) -> Unit raise ProtocolError

    Accept any boundary, including inside the four-byte length prefix.

    ParamsDecoder::finish

    fn ParamsDecoder::finish(self : ParamsDecoder) -> Array[(Bytes, Bytes)] raise ProtocolError

    Explicit stream terminator. EOF or an empty feed is not a terminator.

    ParamsDecoder::new

    fn ParamsDecoder::new(limit? : Int, max_pairs? : Int) -> ParamsDecoder raise ProtocolError

    Record

    pub(all) struct Record {
    kind : Int
    id : Int
    content : Bytes
    } derive(Eq,
    Debug
    )

    A protocol record; content is immutable and independent of decoder storage.

    Record::equal

    fn Record::equal(Record, Record) -> Bool

    Record::not_equal

    fn Record::not_equal(x : Record, y : Record) -> Bool

    Record::to_repr

    Request

    pub struct Request {
    id : Int
    token : Int64
    params : Array[(Bytes, Bytes)]
    body : Bytes
    // private fields
    }

    A fully buffered, bounded request; token prevents stale completion after reuse.

    Request::param

    fn Request::param(self : Request, name : String) -> Bytes?

    Return the last parameter value (duplicates remain available via params).

    Request::text_param

    fn Request::text_param(self : Request, name : String) -> String? raise

    Strict UTF-8 conversion only on explicit application request.

    RequestHead

    pub struct RequestHead {
    id : Int
    token : Int64
    role : Role
    params : Array[(Bytes, Bytes)]
    // private fields
    }

    RequestHead::param

    fn RequestHead::param(self : RequestHead, name : String) -> Bytes?

    RequestHead::text_param

    fn RequestHead::text_param(self : RequestHead, name : String) -> String? raise

    Response

    pub struct Response {
    status : Int
    headers : Array[(String, String)]
    body : Bytes
    stderr : Bytes
    }

    Response headers are ordered; duplicate Set-Cookie headers are preserved.

    Response::cgi_bytes

    fn Response::cgi_bytes(self : Response, head? : Bool) -> Bytes raise ProtocolError

    Encode CGI headers, followed by the binary body (not an HTTP status line).

    Response::cgi_headers

    fn Response::cgi_headers(self : Response, content_length? : Int) -> Bytes raise ProtocolError

    CGI headers for a streaming response. Omit length when it is not known.

    Response::new

    fn Response::new(body : Bytes, status? : Int, headers? : Array[(String, String)], stderr? : Bytes) -> Response raise ProtocolError

    Construct a response. Validation also runs at serialization to guard mutation.

    Role

    pub(all) enum Role {
    Responder
    Authorizer
    Filter
    } derive(Eq)

    Role::equal

    fn Role::equal(Role, Role) -> Bool

    Role::not_equal

    fn Role::not_equal(x : Role, y : Role) -> Bool

    Session

    pub struct Session {
    // private fields
    }

    Session::accept

    fn Session::accept(self : Session, record : Record) -> Array[Event] raise ProtocolError

    A malformed record poisons this session. The adapter must close the transport.

    Session::close

    fn Session::close(self : Session) -> Unit

    Connection cleanup; no protocol writes are possible after this call.

    Session::complete

    fn Session::complete(self : Session, request : Request, response : Response) -> Array[Event] raise ProtocolError

    Finish only the current generation; aborted or stale completions are ignored.

    Session::finish

    fn Session::finish(self : Session) -> Unit raise ProtocolError

    Session::new

    fn Session::new(limits? : Limits) -> Session raise ProtocolError

    StreamEvent

    pub(all) enum StreamEvent {
    Send(Array[Record])
    Start(RequestHead)
    Input(Int64, Int, Bytes)
    InputEnd(Int64, Int)
    Aborted(Int64)
    Close
    }

    StreamSession

    pub struct StreamSession {
    // private fields
    }

    Socket-independent streaming state machine. One request per connection.

    StreamSession::accept

    fn StreamSession::accept(self : StreamSession, record : Record) -> Array[StreamEvent] raise ProtocolError

    StreamSession::can_output

    fn StreamSession::can_output(self : StreamSession, head : RequestHead) -> Bool

    StreamSession::close

    fn StreamSession::close(self : StreamSession) -> Unit

    StreamSession::complete

    fn StreamSession::complete(self : StreamSession, head : RequestHead, app_status? : UInt) -> Array[StreamEvent]

    StreamSession::finish

    fn StreamSession::finish(self : StreamSession) -> Unit raise ProtocolError

    StreamSession::is_active

    fn StreamSession::is_active(self : StreamSession, head : RequestHead) -> Bool

    StreamSession::is_idle

    fn StreamSession::is_idle(self : StreamSession) -> Bool

    True only after both input streams and the response have completed.

    StreamSession::new

    fn StreamSession::new(limits? : Limits, roles? : Array[Role]) -> StreamSession raise

    encode_params

    fn encode_params(pairs : Array[(Bytes, Bytes)]) -> Bytes

    Byte pairs deliberately preserve duplicate keys and non-UTF8 values.

    encode_record

    fn encode_record(record : Record) -> Bytes raise ProtocolError

    Encode one record with 8-byte alignment padding.

    end_record

    fn end_record(id : Int, app_status? : UInt, protocol_status? : Int) -> Record

    stream_records

    fn stream_records(kind : Int, id : Int, content : Bytes) -> Array[Record]

    Split bytes into stream records, followed by exactly one empty terminator.