Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AFast

简体中文 | English

AFast 是一个高性能 Rust Web 后端框架。用 #[handler] 标注函数即可——框架自动注册路由、通过紧凑二进制协议分发请求,并一键生成 TypeScript / JavaScript / Kotlin / Rust 客户端代码。

亮点

  • 零路由定义#[handler] 标注函数,无需手动路由表
  • 紧凑二进制协议 — 比 JSON 更小更快,专为内部通信设计
  • 零拷贝 StateState<T> 持有 &'static T,无 per-request clone,不要求 T: Clone
  • 自动代码生成 — TypeScript / JavaScript / Kotlin / Rust 客户端,完整类型定义
  • 交互式 API 文档 — 内置 Web 文档,暗色/亮色主题,在线测试,WS/SSE 调试
  • 多传输层 — WebSocket、HTTP/1.1、HTTP/2、TCP,按需组合
  • TLS / HTTPS — 基于 rustls,ALPN 协商 HTTP/2
  • RESTful 端点#[get]/#[post]/#[put]/#[delete] + JSON
  • WebSocket & SSE#[ws]#[sse] 路由宏
  • 长连接 — 通过 Receiver/Sender 双向流式通信
  • 生命周期钩子before_request/on_response/on_error/on_connect/on_disconnect
  • 请求上下文Ctx<T> 请求级上下文,hook 写入,handler 读取
  • 请求限流 — 命名策略 + 可插拔存储后端

快速示例

use afast::{AFast, handler, service, State, Data, Result};
use afast::{AFastDeserialize, AFastSerialize, Tag};

struct AppState { db_url: String }

#[derive(AFastDeserialize, Tag)]
#[tag("请求体")]
struct HelloReq { name: String }

#[derive(AFastSerialize, Tag)]
#[tag("响应体")]
struct HelloResp { message: String }

#[handler(desc("打招呼"))]
async fn hello(
    state: State<AppState>,
    req: Data<HelloReq>,
) -> Result<HelloResp> {
    Ok(HelloResp { message: format!("Hello, {}!", req.name) })
}

#[tokio::main]
async fn main() {
    let svc = service!("api" => { h(hello) });
    AFast::new()
        .state(AppState { db_url: "localhost".into() })
        .service(svc)
        .http("0.0.0.0:5000")
        .run().await.unwrap();
}

快速开始

添加依赖

[dependencies]
afast = { version = "0.1.24", features = ["http", "ordinary-http", "ts"] }
tokio = { version = "1", features = ["full"] }

定义 State 和 Handler

#![allow(unused)]
fn main() {
use afast::{AFast, handler, service, State, Data, Custom, Result};
use afast::{AFastDeserialize, AFastSerialize, Tag};
use std::sync::{Arc, Mutex};

// State — no Clone required, holds &'static T internally
struct AppState {
    db_url: String,
    counter: Arc<Mutex<u64>>,
}

#[derive(AFastDeserialize, Tag)]
#[tag("Auth info")]
struct AuthCustom { token: i64, platform: String }

#[derive(AFastDeserialize, Tag)]
#[tag("Request body")]
struct HelloReq { name: String }

#[derive(AFastSerialize, Tag)]
#[tag("Response body")]
struct HelloResp { message: String }

#[handler(desc("Say hello"), name("hello"))]
async fn hello(
    state: State<AppState>,
    auth: Custom<AuthCustom>,
    req: Data<HelloReq>,
) -> Result<HelloResp> {
    let mut count = state.counter.lock().unwrap();
    *count += 1;
    Ok(HelloResp { message: format!("Hello, {}! (count: {})", req.name, count) })
}
}

注册路由并运行

#[tokio::main]
async fn main() {
    let svc = service!("api", "Example API" => {
        h(hello),
    });

    AFast::new()
        .state(AppState {
            db_url: "localhost".into(),
            counter: Arc::new(Mutex::new(0)),
        })
        .service(svc)
        .http("0.0.0.0:5000")
        .run().await.unwrap();
}

运行

cargo run
  • HTTP API: POST http://localhost:5000/_api (二进制协议)
  • API 文档: http://localhost:5000/doc
  • 生成的 TS 客户端: ./client/api.ts

多种传输层

#![allow(unused)]
fn main() {
AFast::new()
    .state(app_state)
    .service(svc)
    .ws("0.0.0.0:3001")     // 二进制 WebSocket
    .tcp("0.0.0.0:4001")    // 二进制 TCP
    .http("0.0.0.0:5001")   // HTTP + ordinary 路由
    .run().await.unwrap();
}

或者将 WS 合并到 HTTP(同一端口):

#![allow(unused)]
fn main() {
AFast::new()
    .state(app_state)
    .service(svc)
    .ws("0.0.0.0:5001")
    .http("0.0.0.0:5001")   // 相同端口 → 自动合并
    .run().await.unwrap();
}

Feature 列表

核心

Feature描述依赖
binary二进制协议 (POST /_api、WS 帧、TCP 帧)
httpHTTP 服务器hyper, hyper-util, http-body-util
wsWebSocket 服务器tokio-tungstenite, futures-util, binary
tcpTCP 服务器 (长度前缀帧)binary

代码生成

Feature描述依赖
tsTypeScript 客户端生成 (ESM + 完整类型)
jsJavaScript 客户端生成 (ESM + JSDoc)
ktKotlin 客户端生成
rsRust 客户端生成 (Tokio 异步 / std 同步 TCP)
csC# / .NET 客户端生成 (HttpClient / WebSocket / TCP)
code按需代码生成,端点 /code/{service}/{lang}http

文档

Feature描述依赖
doc交互式 API 文档,端点 /dochttp, js

Ordinary 路由

Feature描述依赖
ordinary-httpRESTful JSON 端点 (#[get]/#[post] 等)http, serde, serde_json
ordinary-ws基于路径的 WebSocket 端点 (#[ws])ws, ordinary-http
ordinary-sseServer-Sent Events 端点 (#[sse])ordinary-http, futures-util

协议选项

Feature描述
seq64WS 请求 ID 使用 i64(默认 i32
len64WS 负载长度使用 u64(默认 u32
tag-u8枚举标签使用 u8(默认)
tag-u16枚举标签使用 u16
tag-u32枚举标签使用 u32

TLS

Feature描述依赖
tls基于 rustls 的 HTTPS,ALPN 协商 HTTP/2,支持 channel 热重载http, tokio-rustls, rustls, rustls-pemfile

可选能力

Feature描述
marker基于标记的条件序列化,通过 AFast::marker() 设置
hook生命周期钩子 (before_request/on_connect 等),支持全局和按服务配置
rate-limit命名策略的速率限制 (FixedWindow/SlidingWindow/TokenBucket)
tlsTLS/HTTPS 支持,基于 rustls 并支持 ALPN

注意: 如果服务端使用了 seq64len64,生成的客户端代码必须使用相同的 Feature,否则会出现协议不匹配。

核心概念

Handler 注册

#[handler] 过程宏在编译时生成以下内容:

  1. 原始函数保持不变
  2. HandlerMeta — 名称、描述、参数列表、返回类型的元数据
  3. HandlerInvoker trait 实现 — 类型擦除的调用器,反序列化参数并调用函数
  4. 一个静态调用器实例 — 由 register! 宏引用
#![allow(unused)]
fn main() {
#[handler(desc("Get user"), name("get_user"))]
async fn get_user_handler(
    state: State<AppState>,
    auth: Custom<Auth>,
    req: Data<UserIdRequest>,
) -> Result<UserInfo> {
    // ...
}
}
  • desc("...") — 设置文档和 JSDoc 注释中使用的描述
  • name("...") — 覆盖客户端方法名(默认为 Rust 函数名)
  • cache(seconds) — 启用客户端缓存
  • rate_limit("policy") — 将 handler 绑定到命名的速率限制策略
  • 其他属性 — 作为自定义属性收集到 HandlerMeta::attrs

自定义属性

你可以在 handler 宏中添加任意属性。它们会作为 Attr 键值对收集到 HandlerMeta::attrs 中:

#![allow(unused)]
fn main() {
#[handler(desc("Create user"), tag("admin"), timeout(30), deprecated)]
async fn create_user(...) -> ... { ... }
}

在运行时,通过 invoker.meta().unwrap().attrs 读取:

#![allow(unused)]
fn main() {
if let Some(meta) = invoker.meta() {
    for attr in meta.attrs {
        match attr.value {
            AttrValue::Str(v) => println!("{} = {}", attr.key, v),
            AttrValue::Int(v) => println!("{} = {}", attr.key, v),
            AttrValue::Bool(v) => println!("{} = {}", attr.key, v),
        }
    }
}
}

值类型会自动推断:

  • 字符串: tag("admin")AttrValue::Str("admin")
  • 整数: timeout(30)AttrValue::Int(30)
  • 布尔: deprecatedAttrValue::Bool(true)

支持两种语法:tag("admin")tag = "admin"

自定义属性也可以在钩子中通过 RequestContext::attrs 访问:

#![allow(unused)]
fn main() {
impl Hook for MyHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        for attr in ctx.attrs {
            if attr.key == "deprecated" {
                eprintln!("WARNING: {} is deprecated", ctx.handler_name);
            }
        }
        None
    }
}
}

多 State 支持

AFast 支持注册多个 State 类型StateMap 使用 TypeId 作为键,每个类型对应一个值。State<T> 持有 &'static T 引用 — 值在启动时通过 Box::leak 分配一次,不会在每次请求时克隆:

#![allow(unused)]
fn main() {
struct DbConfig { url: String }
struct RedisConfig { url: String }
struct AppConfig { name: String }

let app = AFast::new()
    .state(DbConfig { url: "postgres://...".into() })
    .state(RedisConfig { url: "redis://...".into() })
    .state(AppConfig { name: "my-app".into() });

#[handler(desc("Multiple State example"))]
async fn my_handler(
    db: State<DbConfig>,
    redis: State<RedisConfig>,
    config: State<AppConfig>,
) -> Result<()> {
    println!("DB: {}, Redis: {}, App: {}", db.url, redis.url, config.name);
    Ok(())
}
}

如果 handler 引用了未注册的 State 类型,运行时会返回 CODE_STATE_NOT_FOUND 错误。

内部可变性

由于 State<T> 提供的是共享的 &'static T 引用,修改需要使用内部可变性模式。将可变字段包装在 Arc<Mutex<...>>Arc<RwLock<...>> 中:

#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};

struct AppState {
    db: Arc<Mutex<Database>>,
    counter: Arc<Mutex<u64>>,
}

#[handler(desc("Increment counter"))]
async fn increment(state: State<AppState>) -> Result<()> {
    let mut count = state.counter.lock().unwrap();
    *count += 1;
    Ok(())
}
}

State<T> 不再要求 T: Clone — 只需 T: 'static

多 Data 参数

Handler 可以接受多个 Data<T> 参数,从二进制负载中按顺序反序列化:

#![allow(unused)]
fn main() {
#[derive(AFastDeserialize, Tag)]
#[tag("Pagination")]
struct PageRequest { page: i64, size: i64 }

#[derive(AFastDeserialize, Tag)]
#[tag("Filter")]
struct FilterRequest { keyword: String, status: i32 }

#[handler(desc("Search users"))]
async fn search_users(
    page: Data<PageRequest>,
    filter: Data<FilterRequest>,
) -> Result<PageResponse> {
    // ...
}
}

生成的 TypeScript 客户端方法签名:

async searchUsers(page: PageRequest, filter: FilterRequest): Promise<PageResponse>

自定义错误类型

afast::Result<T> 默认错误类型为 Error,但所有 handler 宏均支持自定义错误类型。实现 AFastError trait 即可:

#![allow(unused)]
fn main() {
use afast::AFastError;

enum AppError {
    NotFound { resource: String },
    Forbidden { reason: String },
}

impl AFastError for AppError {
    fn code(&self) -> i64 {
        match self {
            AppError::NotFound { .. } => 404,
            AppError::Forbidden { .. } => 403,
        }
    }

    fn message(&self) -> String {
        match self {
            AppError::NotFound { resource } => format!("{} not found", resource),
            AppError::Forbidden { reason } => reason.clone(),
        }
    }
}
}

然后在 handler 中使用 afast::Result<T, AppError>

#![allow(unused)]
fn main() {
#[handler(desc("Get user"))]
async fn get_user(id: Data<UserId>) -> afast::Result<UserInfo, AppError> {
    let user = find_user(&id).ok_or(AppError::NotFound { resource: "user".into() })?;
    Ok(user)
}
}

适用于所有宏:#[handler]#[get]/#[post]/#[put]/#[delete]#[ws]#[sse]

使用 afast::Result<T>(不指定第二个参数)等价于 Result<T, Error>,完全向后兼容。

提取器类型

提取器描述协议
State<T>从 StateMap 按类型注入共享状态 (T: ’static, 零拷贝 &’static T)所有
Ctx<T>注入钩子设置的请求上下文数据 (T: Clone)所有
Data<T>从二进制负载反序列化请求体HTTP/WS/TCP
Custom<T>反序列化客户端自定义上下文(如认证令牌)HTTP/WS/TCP
Receiver接收来自客户端的二进制消息(长连接)WS/TCP
Sender向客户端发送二进制消息(长连接)WS/TCP
Query<T>从 URL 查询字符串反序列化(需要 ordinary-httpHTTP
Param<T>从路由路径参数 (:id) 反序列化(需要 ordinary-httpHTTP
Body<T>从 HTTP JSON 请求体反序列化(需要 ordinary-httpHTTP
Header<T>从 HTTP 请求头反序列化(需要 ordinary-httpHTTP
FullPath获取完整请求路径,如 /users/123(需要 ordinary-httpHTTP

服务与嵌套

service! 宏通过 group 构建 handler 树,实现命名空间管理:

#![allow(unused)]
fn main() {
let api_svc = service!("api", "User API" => {
    h(health),
    group("user" => {
        h(list_users),
        h(get_user),
        group("posts" => {
            h(list_posts),
        }),
    }),
    group("chat" => {
        h(chat),  // 使用 Receiver/Sender 的持久连接
    }),
});
}

客户端命名空间路径变为 api.user.list_usersapi.chat.chat 等。

group 内可以混合使用二进制和 ordinary HTTP 路由:

#![allow(unused)]
fn main() {
group("user" => {
    h(get_user),                 // 二进制 handler
    get(":id", get_user_by_id),  // GET /user/:id
    post("", create_user),       // POST /user
    delete(":id", delete_user),  // DELETE /user/:id
}),
}

同名服务合并

注册多个同名服务时,后续的 handler 和路由会自动合并到第一个服务中:

#![allow(unused)]
fn main() {
let user_svc = service!("api", "User API" => {
    h(list_users),
    h(create_user),
});

let user_extra_svc = service!("api" => {
    h(delete_user),
    get(":id", get_user_http),
});

let app = AFast::new()
    .service(user_svc)
    .service(user_extra_svc);  // 合并到 "api"
}

空名称服务

名称为空字符串 ("") 的服务注册的 handler 可通过二进制协议调用,但会从客户端代码生成和 API 文档中排除:

#![allow(unused)]
fn main() {
let internal_svc = service!("", "Internal" => {
    h(debug_info),
    get("ping", ping),
});
}

Catch-all 路由

使用 **name 语法注册 catch-all 路由,捕获所有未匹配其他路由的请求:

#![allow(unused)]
fn main() {
let svc = service!("api" => {
    get("users/:id", get_user),          // 精确匹配优先
    get("*", catch_all_get),             // 捕获其余所有 GET
    post("*path", catch_all_post),       // 捕获其余所有 POST,路径存入 "path"
});
}

匹配优先级(从高到低):

  1. 精确路由(如 /users/list
  2. 参数路由(如 /users/:id
  3. Catch-all 路由(**name

即使 catch-all 最先注册,具体路由仍然优先匹配。内置端点(/_api/_ws/code/doc)不会被 catch-all 拦截。

Catch-all 捕获的路径可通过 Param 提取器获取:

#![allow(unused)]
fn main() {
use std::collections::HashMap;
use afast::{Param, Json, FullPath};

#[get(desc("Catch-all handler"))]
async fn catch_all_get(
    path: FullPath,
    Param(params): Param<HashMap<String, String>>,
) -> Json<serde_json::Value> {
    let rest = params.get("*").unwrap(); // 或 params.get("path") 如果写的是 *path
    Json(serde_json::json!({ "full_path": path.0, "remaining": rest }))
}
}

类型标签

#[derive(Tag)] 为结构体和枚举生成运行时类型元数据。代码生成器通过 FieldMeta.structure 函数指针递归发现嵌套类型:

#![allow(unused)]
fn main() {
use afast::Tag;

#[derive(Tag)]
#[tag("User role")]
enum Role {
    Admin,
    User { level: i32 },
    Guest { expires_at: i64 },
    Custom(String),
}

#[derive(Tag)]
#[tag("User info")]
struct User {
    name: String,
    role: Role,           // 自动递归发现 Role 的字段
    tags: Vec<String>,    // Vec 元素类型自动展开
    avatar: Option<Vec<u8>>,
}
}

验证规则

规则示例描述
gt(value, code, "msg")#[afast(gt(0, 400, "must > 0"))]大于
gte(value, code, "msg")#[afast(gte(1, 400, "must >= 1"))]大于等于
lt(value, code, "msg")#[afast(lt(100, 400, "must < 100"))]小于
lte(value, code, "msg")#[afast(lte(99, 400, "must <= 99"))]小于等于
len(min, max, code, "msg")#[afast(len(1, 20, 400, "len 1-20"))]长度约束
of(["a","b"], code, "msg")#[afast(of(["a","b"], 400, "a or b"))]枚举值

条件序列化 (Marker)

当启用 marker Feature 时,AFast::marker() 设置一个全局标记字符串(默认 "afast"),传递给 afastdata 的 to_bytes_with / from_bytes_with。使用 #[afast(skip_with("marker"))] 标注的字段会根据当前激活的标记在序列化/反序列化时被条件跳过。

跳过模式

  • #[afast(skip)] — 字段始终跳过,永不序列化/反序列化。必须有 Default 实现或初始化函数。
  • #[afast(skip_with("marker"))] — 当标记匹配时跳过;否则正常序列化。

标记会递归传播到嵌套类型(Vec<T>Option<T> 等)。

生成的客户端代码(TS/JS/KT/RS)和 API 文档会自动排除被跳过的字段。

示例

#![allow(unused)]
fn main() {
#[derive(AFastSerialize, AFastDeserialize, Tag)]
#[tag("User info")]
struct User {
    name: String,
    #[afast(skip)]
    internal_secret: String,        // 始终跳过
    #[afast(skip_with("afast"))]
    internal_note: String,          // 当 marker 为 "afast" 时跳过
}

let app = AFast::new()
    .marker("afast")  // 设置标记;默认已经是 "afast"
    .service(svc)
    .http("0.0.0.0:5000");
}

不启用 marker Feature 时,serialize / deserialize 使用普通的 to_bytes / from_bytes,所有字段始终包含。但 #[afast(skip)] 字段仍会从生成的客户端代码中排除。

速率限制

启用 rate-limit Feature 可为 handler 应用命名的速率限制策略。支持 HTTP、WebSocket 和 TCP 传输层。

配置

#![allow(unused)]
fn main() {
use afast::{RateLimitConfig, RateLimitPolicy, RateLimitKey, Algorithm};

let app = AFast::new()
    .rate_limit(
        RateLimitConfig::new()
            .policy(RateLimitPolicy {
                id: "login".into(),
                max_requests: 5,
                window_secs: 60,
                key: RateLimitKey::Ip,
                algorithm: Algorithm::SlidingWindow,
            })
            .default_policy("global")
            .policy(RateLimitPolicy {
                id: "global".into(),
                max_requests: 100,
                window_secs: 1,
                key: RateLimitKey::Ip,
                algorithm: Algorithm::SlidingWindow,
            }),
    )
    .service(svc)
    .http("0.0.0.0:5000");
}

绑定 Handler

#![allow(unused)]
fn main() {
#[handler(rate_limit("login"), desc("User login"))]
async fn login(
    state: State<AppState>,
    req: Data<LoginRequest>,
) -> Result<LoginResponse> {
    // ...
}
}

没有 rate_limit 的 handler 自动使用 default_policy。如果未设置默认策略,则不受速率限制。

速率限制键

描述HTTPWebSocketTCP
Ip客户端 IP(支持 X-Forwarded-For
Header("name")HTTP 头值(如 API Key)✅ (握手时缓存)⏭ 跳过
Connection按连接(WS/TCP 消息速率)⏭ 跳过
Global共享全局计数器

存储后端

默认的 InMemoryStore 在进程内存中保存计数器。实现 RateLimitStore 可使用自定义后端(如 Redis):

#![allow(unused)]
fn main() {
use afast::RateLimitStore;

struct RedisStore { /* ... */ }

impl RateLimitStore for RedisStore {
    fn incr<'a>(&'a self, key: &'a str, ttl_secs: u64)
        -> Pin<Box<dyn Future<Output = u64> + Send + 'a>> { /* INCR + EXPIRE */ }
    fn get<'a>(&'a self, key: &'a str)
        -> Pin<Box<dyn Future<Output = u64> + Send + 'a>> { /* GET */ }
    fn set<'a>(&'a self, key: &'a str, value: u64, ttl_secs: u64)
        -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> { /* SET + EXPIRE */ }
    fn delete<'a>(&'a self, key: &'a str)
        -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> { /* DEL */ }
}
}

拒绝响应

  • HTTP: 状态码 429 Too Many Requests,响应体: {"code":-90012,"message":"Too many requests"}
  • WebSocket / TCP: 错误帧,错误码 -90012

可通过 RateLimitConfig::rejected_code()rejected_message() 自定义。

生命周期钩子

启用 hook Feature 可拦截请求生命周期事件,用于可观测性、链路追踪、日志记录或自定义中间件。

快速示例

#![allow(unused)]
fn main() {
use afast::hook::{Hook, RequestContext, RequestGuard, ConnectionGuard};

struct LoggingHook;

impl Hook for LoggingHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        println!("→ {} ({})", ctx.handler_name, ctx.transport);
        Some(Box::new(std::time::Instant::now()))
    }

    fn on_connect(&self, ctx: &RequestContext) -> Option<Box<dyn ConnectionGuard>> {
        println!("↕ connect: {} ({})", ctx.handler_name, ctx.transport);
        Some(Box::new(()))
    }
}

impl RequestGuard for std::time::Instant {
    fn on_response(&mut self, ctx: &RequestContext, _resp: &[u8]) {
        println!("← {} OK ({:?})", ctx.handler_name, self.elapsed());
    }
    fn on_error(&mut self, ctx: &RequestContext, err: &afast::Error) {
        println!("✗ {} error: {}", ctx.handler_name, err);
    }
}

impl ConnectionGuard for () {
    fn on_disconnect(&mut self, ctx: &RequestContext) {
        println!("✕ disconnect: {} ({})", ctx.handler_name, ctx.transport);
    }
}
}

钩子 Trait

Hook — 入口点

#![allow(unused)]
fn main() {
pub trait Hook: Send + Sync + 'static {
    /// Called before each request for request-response interfaces
    /// (HTTP binary, WS binary, TCP binary, ordinary HTTP).
    /// Return a `RequestGuard` to observe the response.
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> { None }

    /// Called when a connection is established for connection-oriented interfaces
    /// (WS long, TCP long, ordinary WS, SSE).
    /// Return a `ConnectionGuard` to observe disconnection.
    fn on_connect(&self, ctx: &RequestContext) -> Option<Box<dyn ConnectionGuard>> { None }
}
}

RequestGuard — 每请求观察者

#![allow(unused)]
fn main() {
pub trait RequestGuard: Send + 'static {
    /// Called when the handler returns Ok.
    fn on_response(&mut self, ctx: &RequestContext, response: &[u8]) {}

    /// Called when the handler returns Err.
    fn on_error(&mut self, ctx: &RequestContext, error: &afast::Error) {}
}
}

ConnectionGuard — 长连接观察者

#![allow(unused)]
fn main() {
pub trait ConnectionGuard: Send + 'static {
    /// Called when the connection is closed.
    fn on_disconnect(&mut self, ctx: &RequestContext) {}
}
}

全局钩子和服务钩子

#![allow(unused)]
fn main() {
let app = AFast::new()
    .hook(LoggingHook)                   // 全局:所有 handler
    .service(
        service!("api" => { h(handler) })
            .hook(ApiSpecificHook)       // 服务级:仅该服务的 handler
    );
}
  • 全局钩子对所有服务中的每个 handler 运行。
  • 服务钩子仅对该服务中的 handler 运行。
  • 两者始终执行 — 不会互相替代。
  • 执行顺序:先全局,后服务(洋葱模型)。

按传输层的钩子生命周期

钩子按接口类型分为两类:

  • before_request(请求-响应):HTTP 二进制、WS 二进制、TCP 二进制、ordinary HTTP。
  • on_connect(面向连接):WS 长连接、TCP 长连接、ordinary WS、SSE。

二进制协议 (HTTP POST /_api、WS /_ws、TCP)

普通 handler(请求-响应):

before_request → handler → on_response / on_error

长连接 handler (call_stream):

on_connect → handler → on_disconnect

Ordinary HTTP (ordinary-http)

before_request → handler → on_response / on_error

ordinary HTTP 不会调用 on_connect / on_disconnect(无状态请求-响应)。

Ordinary WebSocket (ordinary-ws)

on_connect → handler → on_disconnect
  • on_connect:在 WebSocket 握手完成后触发。
  • on_disconnect:在 handler 返回且转发任务清理完成后触发。

Ordinary SSE (ordinary-sse)

on_connect → handler (spawned) → on_disconnect
  • on_connect:在 SSE 响应发送和 handler 生成之前触发。
  • on_disconnect:在 handler 任务完成后触发。

请求上下文集成

钩子可以通过 RequestContext 上的 ctx 字段读写请求数据。然后 handler 可以通过 Ctx<T> 提取器访问这些数据。

#![allow(unused)]
fn main() {
use afast::hook::{Hook, RequestContext};

#[derive(Clone)]
struct RequestId(pub String);

struct CtxHook;

impl Hook for CtxHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        // 写入请求上下文
        ctx.ctx.insert(RequestId(format!("req-{:08x}", /* ... */)));
        None
    }
}
}

Handler 自动获取:

#![allow(unused)]
fn main() {
#[handler(desc("..."))]
async fn my_handler(ctx: afast::Ctx<RequestId>) -> afast::Result<()> {
    println!("request_id = {}", ctx.0 .0);
    Ok(())
}
}

同一个请求的所有钩子和 handler 共享同一个上下文。对于长连接 handler(WS/TCP),上下文在整个连接期间持续存在。

详见 请求上下文 (Ctx)

访问自定义属性

RequestContext 通过 ctx.attrs 暴露 handler 的自定义属性:

#![allow(unused)]
fn main() {
impl Hook for DeprecationHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        for attr in ctx.attrs {
            match attr.key {
                "deprecated" => eprintln!("WARNING: {} is deprecated", ctx.handler_name),
                "tag" => {
                    if let AttrValue::Str(v) = attr.value {
                        eprintln!("tag: {}", v);
                    }
                }
                _ => {}
            }
        }
        None
    }
}
}

获取客户端 IP

RequestContext 提供两个字段用于获取客户端 IP:

#![allow(unused)]
fn main() {
impl Hook for IpHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        // TCP 连接的对端 IP
        let peer_ip = &ctx.client_ip;

        // 真实客户端 IP(从 X-Forwarded-For / X-Real-IP 头获取)
        let real_ip = ctx.forwarded_for.as_deref().unwrap_or(&ctx.client_ip);

        println!("client: {}, real: {}", peer_ip, real_ip);
        None
    }
}
}
  • client_ip:所有传输层均有值
  • forwarded_for:仅 HTTP/WS 有值,TCP 为 None

钩子键 — 路由匹配

钩子通过 "service_name:route_path" 匹配,而不是通过 handler 函数名。这避免了同一服务中不同 group 内出现相同函数名时的冲突:

#![allow(unused)]
fn main() {
service!("admin" => {
    group("users" => {
        get("info", get_info),    // key: "admin:/users/info"
    }),
    group("posts" => {
        get("info", get_info),    // key: "admin:/posts/info" — 无冲突!
    }),
})
}

对于合并的服务(同名服务多次注册),钩子条目会自动去重。

RequestContext 字段

字段类型描述
handler_name&'static strHandler 函数名
handler_desc&'static str#[handler(desc(...))] 中的描述
transport&'static str"http-binary""http""ws-binary""ws""tcp""sse"
is_binarybool是否为二进制协议 handler
method&'static strHTTP 方法 ("GET""POST" 等),非 HTTP 时为空
long_connectionbool是否为长连接 handler (Receiver/Sender)
handler_idusizeHandler 在二进制分发表中的偏移量(ordinary 路由为 0)
stateArc<StateMap>共享应用状态
ctxRequestCtx请求上下文容器(钩子写入,handler 通过 Ctx<T> 读取)
attrs&'static [Attr]#[handler(...)] 中的自定义 handler 属性

支持的提取器

所有提取器在所有传输层上均可工作:

提取器HTTPWSSSETCP
State<T>
Query<T>
Param<T>
Header<T>
Body<T>
Custom<T>
Data
WsSender
WsReceiver
SseSender
Sender
Receiver

服务端示例输出

[hook] ↕ connect: chat_ws (ws)       ← on_connect
[check-svc] ▶ chat_ws                ← 服务钩子
[ws-chat] client joined room: test   ← handler 运行
[ws-chat] client left room: test     ← handler 返回
[hook] ✕ disconnect: chat_ws (ws)    ← on_disconnect
[check-svc] ◀ chat_ws done           ← 服务钩子完成

请求上下文 (Ctx)

Ctx<T> 提取器提供按请求(或按连接)的类型化数据,在整个 handler 生命周期中流转。与应用全局的 State<T> 不同,Ctx<T> 的作用域限于单个请求。

核心概念

State<T>Ctx<T>
作用域应用全局按请求 / 按连接
存储StateMap(启动时设置一次)RequestCtx(每个请求创建)
生命周期整个应用请求开始 → 完全结束
用途数据库连接池、配置请求 ID、认证信息、计时
设置方AFast::state()钩子 (before_requeston_connect)

工作原理

请求到达
    │
    ▼
RequestCtx::new()          ← 框架创建空上下文
    │
    ▼
Hook: before_request()     ← 钩子插入值: ctx.ctx.insert(RequestId(...))
    │
    ▼
Handler 执行               ← 框架提取: Ctx<RequestId> 从上下文中
    │
    ▼
Hook: on_response()        ← 钩子读取值: ctx.ctx.get::<RequestId>()
    │
    ▼
RequestCtx 释放             ← 所有值被释放

对于长连接 handler(WS/TCP),RequestCtx 在整个连接期间持续存在:

连接建立
    │
    ▼
RequestCtx::new()          ← 创建一次
    │
    ▼
Hook: on_connect()         ← 钩子插入连接级数据
    │
    ▼
消息 1: handler 执行        ← 读取 Ctx<T>
消息 2: handler 执行        ← 相同的 Ctx<T>(消息间共享)
    ...
    │
    ▼
Hook: on_disconnect()      ← 钩子读取最终状态
    │
    ▼
RequestCtx 释放

快速示例

1. 定义上下文数据

#![allow(unused)]
fn main() {
#[derive(Clone)]
pub struct RequestInfo {
    pub request_id: String,
    pub started_at: std::time::Instant,
}
}

类型必须实现 Clone + Send + Sync + 'static(用于通过 Ctx<T> 提取)。

2. 创建插入数据的钩子

#![allow(unused)]
fn main() {
use afast::hook::{Hook, RequestContext, RequestGuard};

struct CtxHook;

impl Hook for CtxHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        ctx.ctx.insert(RequestInfo {
            request_id: format!("req-{:08x}", /* ... */),
            started_at: std::time::Instant::now(),
        });
        None  // 如果只是写入上下文,不需要 guard
    }
}
}

3. 在 handler 中使用

#![allow(unused)]
fn main() {
use afast::{Ctx, handler};

#[handler(desc("My handler"))]
async fn my_handler(ctx: Ctx<RequestInfo>) -> afast::Result<MyResp> {
    let elapsed = ctx.0.started_at.elapsed();
    println!("request {} took {:?}", ctx.0.request_id, elapsed);
    // ...
}
}

ctx.0 给你内部的 RequestInfo 值。

支持的 Handler 类型

Ctx<T>所有 handler 类型中均可使用:

Handler 类型示例
二进制协议#[handler]async fn h(ctx: Ctx<Info>) -> Result<T>
HTTP ordinary#[get] / #[post]async fn h(ctx: Ctx<Info>) -> HttpResult<Json<T>>
WebSocket#[ws]async fn h(ctx: Ctx<Info>, sender: WsSender) -> Result<()>
SSE#[sse]async fn h(ctx: Ctx<Info>, sender: SseSender) -> Result<()>
长连接#[handler] + Receiver/Senderasync fn h(ctx: Ctx<Info>, rx: Receiver, tx: Sender)

Ctx<T> 不参与二进制/ordinary 互斥检查,因此可以与任何其他提取器组合使用。

参数位置

Ctx<T> 可以放在参数列表的任意位置。按惯例放在第一位:

#![allow(unused)]
fn main() {
#[handler(desc("..."))]
async fn my_handler(
    ctx: Ctx<RequestInfo>,          // ← 上下文在前
    state: State<AppState>,          // ← 然后是 state
    data: Data<MyReq>,               // ← 最后是 data
) -> afast::Result<MyResp> {
    // ...
}
}

在钩子中读写

钩子通过 RequestContext::ctx 与上下文交互:

#![allow(unused)]
fn main() {
impl Hook for MyHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        // 写入
        ctx.ctx.insert(MyData { value: 42 });

        // 读取(如果其他钩子之前写入了数据)
        // ctx.ctx.get::<OtherData>()

        Some(Box::new(MyGuard))
    }
}

impl RequestGuard for MyGuard {
    fn on_response(&mut self, ctx: &RequestContext, _resp: &[u8]) {
        // 读取 handler 或之前钩子写入的数据
        if let Some(data) = ctx.ctx.get::<MyData>() {
            println!("value was {}", data.value);
        }
    }
}
}

获取客户端 IP

RequestContext 提供两个字段用于获取客户端 IP 地址:

字段类型说明
client_ipStringTCP 连接的对端 IP(peer_addr
forwarded_forOption<String>X-Forwarded-For / X-Real-IP 头获取的真实 IP
#![allow(unused)]
fn main() {
impl Hook for MyHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        // 直接连接的 IP(可能是代理 IP)
        let ip = &ctx.client_ip;

        // 真实客户端 IP(仅 HTTP/WS 有值)
        let real_ip = ctx.forwarded_for.as_deref().unwrap_or(&ctx.client_ip);

        println!("client: {} (real: {})", ip, real_ip);
        None
    }
}
}

说明

  • client_ip 在所有传输层(HTTP、WS、TCP、SSE)均有值
  • forwarded_for 仅在 HTTP 和 WebSocket 传输层有值,TCP 始终为 None
  • 当处于反向代理后面时,建议优先使用 forwarded_for

API 参考

RequestCtx — 容器

#![allow(unused)]
fn main() {
// 创建空上下文
let ctx = RequestCtx::new();

// 插入值(按类型为键)
ctx.insert(my_value);

// 获取克隆的值
let val: Option<MyType> = ctx.get::<MyType>();

// 克隆(廉价 — 共享 Arc)
let ctx2 = ctx.clone();
}

Ctx<T> — 提取器

#![allow(unused)]
fn main() {
pub struct Ctx<T>(pub T);

// 访问内部值
let inner: T = my_ctx.0;
}

性能

  • RequestCtx::new() 分配单个 Arc<RwLock<HashMap>> — 非常廉价。
  • insert()get() 获取 RwLock — 在典型场景(先顺序写入再读取)下无竞争,开销可忽略。
  • RequestCtx::clone()Arc 克隆 — O(1)。
  • 对于大型上下文值,用 Arc<T> 包装可使 get() 的克隆更廉价。
  • 不使用 Ctx<T> 的 handler 零提取成本 — 空上下文被创建但不会被读取。

传输层

AFast 支持多种传输层,可以在不同端口上同时运行。

HTTP

HTTP 服务端点:

方法路径描述
POST/_api二进制 handler 分发
GET/_wsWebSocket 升级(合并模式)
GET/code/{service}/{lang}按需代码生成(需要 code
GET/docAPI 文档索引(需要 doc
GET/doc/{service}服务特定文档(需要 doc
*ordinary 路由RESTful 端点(需要 ordinary-http
GET/path/:paramordinary-ws 路由的 WebSocket 升级(需要 ordinary-ws
GET/pathordinary-sse 路由的 SSE 流(需要 ordinary-sse

HTTP 响应格式:

  • 成功: [0u8][0i64][data: bytes]
  • 错误: [1u8][code: i64][message: bytes]

WebSocket

WS 帧格式:

Request:  [req_id: SeqId][handler_id: u32][len: Len][payload]
Push:     [0: SeqId][conn_id: u32][len: Len][payload]
Heartbeat:[0xFFFFFFFF: SeqId][len: Len][conn_id1: u32]...

SeqId 类型由 seq64 Feature 控制(i32i64),Len 类型由 len64 Feature 控制(u32u64)。

TCP

TCP 使用 4 字节大端序长度前缀帧,每帧包含完整的二进制负载。适用于嵌入式设备或原生 TCP 场景。

HTTP + WS 端口合并

ws_addrhttp_addr 设置为相同地址时,AFast 通过 HTTP Upgrade 将 WebSocket 合并到 HTTP 服务器中:

#![allow(unused)]
fn main() {
// 相同端口同时用于 HTTP 和 WebSocket
let app = AFast::new()
    .service(svc)
    .ws("0.0.0.0:5000")
    .http("0.0.0.0:5000");  // 相同地址,自动合并
}

TLS / HTTPS

AFast 支持基于 rustls 的 TLS/HTTPS,ALPN 协商 HTTP/2。

基本用法

#![allow(unused)]
fn main() {
let app = AFast::new()
    .service(svc)
    .https("0.0.0.0:5443", "./cert.pem", "./key.pem", None);
}

优雅降级

如果证书文件不存在,服务器自动降级为普通 HTTP:

afast: TLS cert files not found, starting without encryption: [::]:5443

热重载证书

通过 broadcast channel 在运行时重载证书,无需重启:

#![allow(unused)]
fn main() {
let (reload_tx, reload_rx) = tokio::sync::broadcast::channel(1);

let app = AFast::new()
    .https("0.0.0.0:5443", "./cert.pem", "./key.pem", Some(reload_rx));

// 使用原始路径重载
reload_tx.send(None).unwrap();

// 使用新路径重载
reload_tx.send(Some(TlsReloadMessage {
    cert_path: "/new/cert.pem".into(),
    key_path: "/new/key.pem".into(),
})).unwrap();
}

Ordinary HTTP (REST)

使用 ordinary-http,在 service! 宏内使用 get/post/put/patch/delete 定义 RESTful 路由:

#![allow(unused)]
fn main() {
use afast::{get, Query, Param, Body, Header, Json, HttpResult};

#[derive(Deserialize)]
struct UserQuery { page: i64, size: i64 }

#[derive(Serialize)]
struct UserResponse { id: i64, name: String }

#[get(":id")]
async fn get_user(
    state: State<AppState>,
    param: Param<UserPath>,
    query: Query<UserQuery>,
) -> HttpResult<Json<UserResponse>> {
    Ok(Json(UserResponse { id: param.id, name: format!("User {}", param.id) }))
}
}

响应类型

类型HTTP 状态码Content-Type
Json<T>200application/json
Text200text/plain
Html200text/html
File200自定义 + Content-Disposition: attachment
Status(code)自定义
Redirect::temporary(url) / Redirect::permanent(url)302 / 301Location

CORS

通过 AFast::cors() 启用 CORS(跨源资源共享),需要 http feature。所有 HTTP 端点——包括二进制 /_api、普通 HTTP 路由以及代码/文档端点——都会自动包含 CORS 头:

#![allow(unused)]
fn main() {
use afast::{AFast, CorsConfig};

// 开发环境:允许所有来源
AFast::new()
    .cors(CorsConfig::permissive())
    .http("0.0.0.0:5000")
    .run().await;

// 生产环境:指定来源并启用凭证
AFast::new()
    .cors(
        CorsConfig::new(vec!["https://example.com", "https://app.example.com"])
            .allow_credentials(true)
            .max_age(7200)
    )
    .http("0.0.0.0:5000")
    .run().await;
}

服务器会自动:

  • 响应 OPTIONS 预检请求,返回 204 No Content
  • 在每个 HTTP 响应中注入 Access-Control-Allow-Origin
  • 为预检请求设置 Access-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Max-Age

安全头

每个 HTTP 响应默认包含以下安全头:

头部
x-content-type-optionsnosniff
x-frame-optionsDENY
content-security-policydefault-src 'self'

可通过 AFast::security_headers() 覆盖:

#![allow(unused)]
fn main() {
AFast::new()
    .security_headers(vec![
        ("x-content-type-options", "nosniff"),
        ("x-frame-options", "SAMEORIGIN"),
        ("content-security-policy", "default-src 'self'; script-src 'self' 'unsafe-inline'"),
    ])
    .http("0.0.0.0:5000")
    .run().await;
}

Ordinary WebSocket

使用 ordinary-ws,在 service! 宏内使用 ws 定义 WebSocket 路由。这些路由使用文本/JSON 帧而非二进制协议:

#![allow(unused)]
fn main() {
use afast::{ws, WsSender, WsReceiver, WsParam};

#[derive(Deserialize)]
struct ChatParam { room: String }

#[ws(desc("Chat room"))]
async fn chat_ws(
    param: WsParam<ChatParam>,
    sender: WsSender,
    mut receiver: WsReceiver,
) -> afast::Result<()> {
    while let Some(msg) = receiver.recv().await {
        if let afast::WsMessage::Text(text) = msg {
            sender.send_text(format!("[{}] {}", param.0.room, text)).await?;
        }
    }
    Ok(())
}

let svc = service!("chat" => {
    ws("/chat/:room", chat_ws),
});
}

连接: ws://host:port/chat/general?token=abc

TS/JS 客户端会自动生成平台感知的 WebSocket 连接方法,兼容浏览器、UniApp 和微信小程序。

Server-Sent Events (SSE)

需要 ordinary-sse Feature。使用 sse() 注册 SSE 路由:

#![allow(unused)]
fn main() {
use afast::{SseSender, Query};

#[derive(Deserialize)]
struct SseQuery { room: Option<String> }

#[afast::sse(desc("SSE event stream"))]
async fn notifications(
    query: Query<SseQuery>,
    sender: SseSender,
) -> afast::Result<()> {
    let room = query.0.room.unwrap_or_default();

    // 发送命名事件
    sender.send_event("connected", &serde_json::json!({"room": room})).await?;

    // 发送数据事件(自动序列化为 JSON)
    let mut count = 0u64;
    loop {
        tokio::time::sleep(Duration::from_secs(1)).await;
        count += 1;
        if sender.send_event("tick", &serde_json::json!({"count": count})).await.is_err() {
            break;  // 客户端断开连接
        }
    }
    Ok(())
}

let svc = service!("events" => {
    sse("/notifications", notifications),
});
}

连接: GET http://host:port/notifications?room=general

响应头:

  • Content-Type: text/event-stream; charset=utf-8
  • Cache-Control: no-cache
  • Connection: keep-alive
  • Transfer-Encoding: chunked

线路格式:

event: connected
data: {"room":"general"}

event: tick
data: {"count":1}

SseSender 方法

方法描述
send<T: Serialize>(data)发送 data: 事件,值为 JSON 序列化
send_event<T: Serialize>(event, data)发送命名事件,值为 JSON 序列化

SseEvent 字段

字段类型线路格式
eventOption<&str>event: name\n
dataStringdata: ...\n
idOption<&str>id: ...\n
retryOption<u64>retry: ...\n

客户端代码生成

  • TS/JS: 生成基于 EventSource 的方法
  • KT (OkHttp): 使用 okhttp3.sse.EventSource
  • KT (非 OkHttp): 使用 java.net.http.HttpClient 配合 BodyHandlers.ofLines()
// TS/JS 生成的客户端
const es = client.apis.sse_stream({ room: "general" });
es.addEventListener("connected", (e) => console.log("Connected:", e.data));
es.addEventListener("tick", (e) => console.log("Tick:", JSON.parse(e.data)));
es.close();

长连接

使用 Receiver/Sender 的 handler 会被自动检测为长连接模式:

#![allow(unused)]
fn main() {
#[handler(desc("Chat"))]
async fn chat(
    state: State<AppState>,
    auth: Custom<Auth>,
    mut receiver: Receiver,
    sender: Sender,
) -> Result<()> {
    sender.send(b"Welcome!".to_vec()).await?;
    while let Some(msg) = receiver.recv().await {
        sender.send(msg).await?;  // 回显
    }
    Ok(())
}
}

生成的客户端为长连接 handler 返回 Socket 对象,提供 send()/close() 方法和 onMessage 回调。

文件上传

AFast 通过 multer crate 支持 multipart/form-data 文件上传。提供两种提取模式:

原始 Multipart (Multipart)

直接访问 multipart 流:

#![allow(unused)]
fn main() {
use afast::{post, Multipart, HttpResult, Json, Tag};
use serde::Serialize;

#[derive(Serialize, Tag)]
#[tag("上传结果")]
struct UploadResult {
    filename: String,
    size: usize,
}

#[post(desc("上传文件"))]
async fn upload(mut form: Multipart) -> HttpResult<Json<UploadResult>> {
    let field = form.next_field().await?.ok_or_else(|| {
        afast::Error::custom(400, "未提供文件")
    })?;
    let filename = field.file_name().unwrap_or("unknown").to_string();
    let data = field.bytes().await?;
    Ok(Json(UploadResult { filename, size: data.len() }))
}
}

类型化提取 (MultipartForm<T>)

使用 #[derive(FromFormData)] 自动提取到结构体:

#![allow(unused)]
fn main() {
use afast::{post, MultipartForm, FileField, HttpResult, Json, Tag, FromFormData};
use serde::Serialize;

#[derive(FromFormData, Tag)]
#[tag("上传表单")]
struct UploadForm {
    description: String,
    file: FileField,
}

#[derive(Serialize, Tag)]
#[tag("上传结果")]
struct UploadResult {
    filename: String,
    description: String,
    size: usize,
}

#[post(desc("带表单数据的上传"))]
async fn upload_typed(form: MultipartForm<UploadForm>) -> HttpResult<Json<UploadResult>> {
    let data = form.0;
    Ok(Json(UploadResult {
        filename: data.file.filename.unwrap_or_else(|| "unknown".to_string()),
        description: data.description,
        size: data.file.bytes.len(),
    }))
}
}

#[derive(FromFormData)]

该 derive 宏自动实现 FromFormData trait。每个结构体字段名必须与对应的表单字段名匹配。

支持的字段类型

Rust 类型表单字段说明
String文本字段直接文本值
i8/i16/i32/i64/u8/u16/u32/u64/f32/f64文本字段从文本解析
bool文本字段"true"/"false"/"1"/"0"
FileField文件字段收集字节、文件名、内容类型
Option<T>可选字段缺失时默认为 None

FileField 结构

#![allow(unused)]
fn main() {
pub struct FileField {
    pub name: String,                // 表单字段名
    pub filename: Option<String>,    // 原始文件名
    pub content_type: Option<String>, // MIME 类型
    pub bytes: Vec<u8>,              // 文件内容
}
}

客户端代码生成

TS/JS/KT 代码生成器自动生成基于 FormData 的上传代码:

// 生成的 TypeScript 客户端
const formData = new FormData();
formData.append('description', 'My file');
formData.append('file', new Blob(['content']), 'test.txt');
const result = await client.apis.upload_typed({ body: formData });

使用 curl 测试

# 原始上传
curl -F "file=@test.txt" http://localhost:5001/upload

# 带多个字段的类型化上传
curl -F "description=我的文件" -F "file=@test.txt" http://localhost:5001/upload/typed

代码生成

静态生成(编译时文件输出)

#![allow(unused)]
fn main() {
use afast::{GenerateTarget, Lang, JsTsCallType, RsCallType, NetCallType};

let app = AFast::new()
    .service(api_svc)
    .generate(vec![
        GenerateTarget {
            lang: Lang::TS(vec![JsTsCallType::Fetch, JsTsCallType::Ws]),
            path: "./code".into(),
            debug: false,
        },
        GenerateTarget {
            lang: Lang::RS(vec![RsCallType::TcpAsync]),
            path: "./src/bin/client".into(),
            debug: true,
        },
        GenerateTarget {
            lang: Lang::CS(vec![NetCallType::Http, NetCallType::Ws, NetCallType::Tcp]),
            path: "./client".into(),
            debug: true,
        },
    ]);
}

动态生成(HTTP 端点)

GET /code/api/ts?call=fetch,ws
GET /code/api/js?call=fetch,ws
GET /code/pay/kt?call=http,ws,tcp
GET /code/api/rs?call=tcp-async
GET /code/api/cs?call=http,ws,tcp

支持的传输类型

TS/JS

API
fetch浏览器 fetch
ws浏览器 WebSocket
nodetcpNode.js net
buntcpBun Bun.connect
unirequestUniApp uni.request
uniwsUniApp uni.connectSocket
wxrequest微信小程序 wx.request
wxws微信小程序 wx.connectSocket

Kotlin

API
http / fetchjava.net.HttpURLConnection
wsjava.net.http.WebSocket
tcpjava.net.Socket

Rust

API
tcp-asynctokio::net::TcpStream (异步)
tcp-syncstd::net::TcpStream (同步)

C# / .NET

API
http / fetchSystem.Net.Http.HttpClient
wsSystem.Net.WebSockets.ClientWebSocket
tcpSystem.Net.Sockets.TcpClient

客户端使用

TypeScript / JavaScript

import { ApiClient } from './api';

// 专用 WS 端口
const wsClient = new ApiClient({
  host: 'localhost',
  port: 3001,
  tls: false,
  transport: 'ws',
  debug: false,
});
await wsClient.apis._ready;
const result = await wsClient.apis.user.list_users({ page: 1, size: 20 });

// HTTP (fetch) 模式
const httpClient = new ApiClient({
  host: 'localhost',
  port: 5001,
  tls: false,
  transport: 'fetch',
  debug: false,
});
await httpClient.apis._ready;
const users = await httpClient.apis.user.list_users({ page: 1, size: 20 });

Custom 提取器

通过 customsCustom<T> 提取器提供值:

const client = new ApiClient({
  host: 'localhost',
  port: 5001,
  tls: false,
  transport: 'fetch',
  customs: {
    AuthCustom: () => ({ token: 'my-token' }),
  },
});

Header 提取器

通过 headersHeader<T> 提取器提供值:

const client = new ApiClient({
  host: 'localhost',
  port: 5001,
  tls: false,
  transport: 'fetch',
  headers: {
    AuthHeader: async () => ({ authorization: 'Bearer my-token' }),
  },
});

Kotlin

// HTTP 模式
val client = ApiClient(host = "localhost", port = 5001, tls = false)
val users = client.userListUsers(page = 1, size = 20)

// OkHttp 模式(Android 兼容)
val client = ApiClient(host = "localhost", port = 5001, tls = false, callType = KtCallType.OkHttp)

// WebSocket 模式
val wsClient = ApiClient(host = "localhost", port = 3001, tls = false, callType = KtCallType.Ws)

Rust

#![allow(unused)]
fn main() {
// 异步 TCP 客户端
let mut client = AfastSocket::connect("localhost:4001").await?;
let users: ListUsersResp = client.call(1, &req).await?;

// 同步 TCP 客户端
let mut client = AfastSocketSync::connect("localhost:4001")?;
let users: ListUsersResp = client.call(1, &req)?;
}

C# / .NET

// HTTP 模式
await using var client = new ApiClient("localhost", 5001, false, ApiClient.Transport.Http);
var users = await client.Apis.User.ListUsers(new UserListUsersRequest { Page = 1, Size = 20 });

// WebSocket 模式
await using var wsClient = new ApiClient("localhost", 3001, false, ApiClient.Transport.Ws);
var result = await wsClient.Apis.User.ListUsers(new UserListUsersRequest { Page = 1, Size = 20 });

// TCP 模式
await using var tcpClient = new ApiClient("localhost", 4001, false, ApiClient.Transport.Tcp);
var result = await tcpClient.Apis.User.ListUsers(new UserListUsersRequest { Page = 1, Size = 20 });

// Custom 提取器
client.Customs["AuthCustom"] = async () => new AuthCustom { Token = "my-token" };

客户端传输模式在构造时确定。

注意:普通 HTTP 路由(如 #[get]#[post])仅在 fetch/http 传输模式下可用。WS/TCP 传输仅支持二进制协议 handler(#[handler])。

客户端缓存

cache(seconds) 属性启用客户端缓存:

#![allow(unused)]
fn main() {
#[handler(desc("List users"), cache(60))]
async fn list_users(...) -> Result<ListUsersResponse> { /* ... */ }
}

生成的客户端:

const users = await client.apis.admin.listUsers({ page: 1, size: 20 });
// 60 秒内,相同参数返回缓存数据

const fresh = await client.apis.admin.listUsers({ page: 1, size: 20 }, true);
// force = true 跳过缓存

关于 TextEncoder / TextDecoder

生成的客户端代码使用 TextEncoderTextDecoder API。这些在 React Native(旧版本)、微信小程序和旧浏览器中不可用

解决方案:

  1. Polyfill: npm install text-encoding + import 'text-encoding'
  2. React Native 0.72+: 内置支持
  3. 微信/UniApp: 使用 wxrequest/wxws/unirequest/uniws 传输类型

二进制协议

类型映射

Rust 类型TS/JS 类型Kotlin 类型
i8 ~ i64, u8 ~ u64, f32, f64numberInt/Long/Float/Double
boolbooleanBoolean
String, &strstringString
Vec<u8>Uint8ArrayByteArray
Option<T>T | nullT?
Vec<T>T[]List<T>
struct{ field: Type }data class
enum{ tag: 'Variant', data: ... }sealed class

错误码

系统保留错误码范围为 -90011-90000。用户自定义错误不得使用此范围。

常量描述
CODE_SIGNAL-90000操作系统信号(如 Ctrl+C)
CODE_MSG_TOO_SHORT-90001消息过短
CODE_PAYLOAD_MISMATCH-90002负载长度不匹配
CODE_SERIALIZE-90003序列化/反序列化错误
CODE_STATE_NOT_FOUND-90004State 类型未注册
CODE_HANDLER-90005Handler 执行错误
CODE_INVALID_PARAM-90006无效参数
CODE_IO-90007I/O 错误
CODE_WS-90008WebSocket 错误
CODE_HTTP-90009HTTP 错误
CODE_TCP-90010TCP 错误
CODE_LONG_CONNECTION_NOT_SUPPORTED-90011HTTP 模式不支持长连接
CODE_RATE_LIMITED-90012超出速率限制
#![allow(unused)]
fn main() {
// 自定义错误(错误码必须在保留范围之外)
return Err(afast::Error::custom(400, "invalid request parameter"));
}

自定义错误类型

实现 AFastError trait 即可定义自己的错误类型,handler 直接返回 Result<T, MyError>

#![allow(unused)]
fn main() {
use afast::AFastError;

enum MyError {
    NotFound(String),
    Unauthorized,
}

impl AFastError for MyError {
    fn code(&self) -> i64 {
        match self {
            MyError::NotFound(_) => 404,
            MyError::Unauthorized => 401,
        }
    }

    fn message(&self) -> String {
        match self {
            MyError::NotFound(name) => format!("{} not found", name),
            MyError::Unauthorized => "unauthorized".into(),
        }
    }
}

#[handler(desc("Get user"))]
async fn get_user(id: Data<UserId>) -> afast::Result<UserInfo, MyError> {
    find_user(id).ok_or(MyError::NotFound("user".into()))
}
}

所有 handler 宏(#[handler]#[get]/#[post]/#[put]/#[delete]#[ws]#[sse])均支持自定义错误类型。afast::Result<T> 默认使用 Error,完全向后兼容。

交互式文档

启用 doc Feature 后,访问 http://host:port/doc 可查看交互式 API 文档。

设置

#![allow(unused)]
fn main() {
let app = AFast::new()
    .service(svc)
    .document(afast::DocConfig::with("My API", "./docs"))
    .http("0.0.0.0:5001");
}
  • GET /doc — 索引页,列出所有服务(名称以 _ 开头的服务会被隐藏)
  • GET /doc/{service} — 服务文档,包含类型定义和在线测试面板
  • 深色/浅色主题切换
  • 静态 HTML 文件写入 ./docs 目录

功能

二进制 Handler 测试

每个二进制 handler 显示一个表单,包含:

  • CustomDataState 参数的输入字段
  • 发送按钮,将数据序列化为二进制协议
  • 响应面板,显示反序列化后的结果

Ordinary HTTP 测试

REST 端点(GETPOST 等)显示:

  • 路径参数输入
  • 查询参数输入
  • JSON 请求体编辑器
  • 响应状态码和响应体显示

WebSocket 调试

WS 路由(#[ws])显示调试面板,包含:

  • 路径参数输入(从路由模式自动检测)
  • 查询参数输入
  • 连接/断开按钮
  • 消息输入框和发送按钮(Enter 快捷键)
  • 实时日志面板,显示发送/接收的消息

面板连接到 HTTP 端口(ordinary WS 路由使用 HTTP 升级)。

SSE 调试

SSE 路由(#[sse])显示调试面板,包含:

  • 路径参数输入
  • 查询参数输入
  • 连接/断开按钮
  • 实时事件日志,显示命名事件和数据

配置面板

右上角的设置面板可配置:

  • 传输层: ws(二进制)、fetch(HTTP)、tcp
  • Host: 服务器主机名(默认: localhost
  • Port: 从服务器配置自动检测
  • TLS: 启用安全连接

服务可见性

名称以 _(下划线)开头的服务会从文档索引页隐藏,但仍可通过直接 URL 访问(/doc/_service)。适用于仅内部使用的端点。

项目结构

afast/           — 主框架 crate(核心类型、State、传输层、代码生成)
afast-macros/    — 过程宏(#[handler]、register!、#[derive(Tag)])
example/         — 示例项目(完整用法,包括 HTTP、WS、TCP、文档)

依赖关系

  • afastafast-macrosafastdatatokio
  • afast-macrossynquoteproc-macro2
  • 用户 crate 间接依赖 afastdata-core(由 #[derive(Tag)] 展开代码引用)

测试

单元测试

cargo test --lib

集成测试

示例项目包含所有支持语言的测试客户端:

1. 启动示例服务器

cargo run -p example --bin example

这将启动:

  • HTTP 服务器,端口 5001
  • WebSocket 服务器,端口 3001
  • TCP 服务器,端口 4001
  • API 文档: http://localhost:5001/doc

2. 运行测试客户端

Rust (TCP 传输):

cargo run -p example --bin test_client

JavaScript (Node.js — fetch + ws + nodetcp):

cd client && node test_js.mjs

TypeScript (Bun — fetch + ws + buntcp):

cd client && bun run test_ts.ts

Kotlin (Gradle — HTTP + WS + TCP):

cd client/kt-test && gradle run

Feature 组合

测试框架在各种 Feature 组合下能否正常编译:

cargo check
cargo check --features hook
cargo check --features rate-limit
cargo check --features ordinary-http
cargo check --features ordinary-ws
cargo check --features ordinary-sse
cargo check --features "ordinary-http,ordinary-ws,ordinary-sse,hook,rate-limit"
cargo check --all-features

Clippy

cargo clippy --all-features

许可证

MIT

AFast AI 开发指南

本文档专为 AI 助手设计,帮助 AI 理解如何使用 AFast 框架进行开发。

核心规则

  1. 始终使用 #[handler] — 不要手动注册路由
  2. 使用 Result<T> 处理错误 — 所有 handler 必须返回 Result<T>
  3. 使用 State<T> 管理共享状态 — 零拷贝,无需 clone
  4. 使用 Data<T> 接收请求体 — 自动从二进制负载反序列化
  5. 使用 Custom<T> 获取认证信息 — 客户端提供的认证上下文
  6. 使用 Ctx<T> 获取请求上下文 — 由 hooks 写入,handler 读取
  7. 类型必须派生 Tag — 所有请求/响应类型需要 #[derive(Tag)]#[tag("描述")]
  8. HTTP handler 使用 Header<T> 做认证 — 不要用 Custom<T>

Handler 签名模式

重要:参数必须使用解构语法 afast::X(name): afast::X<Type>

二进制协议 Handler(基础)

#![allow(unused)]
fn main() {
use afast::{AFastDeserialize, AFastSerialize, Tag, handler};
use crate::state::AppState;

#[derive(AFastDeserialize, Tag)]
#[tag("请求体描述")]
pub struct MyRequest {
    #[tag("字段描述")]
    pub name: String,
}

#[derive(AFastSerialize, Tag)]
#[tag("响应体描述")]
pub struct MyResponse {
    #[tag("字段描述")]
    pub message: String,
}

#[handler(desc("描述这个 handler 的功能"))]
pub async fn my_handler(
    afast::State(state): afast::State<AppState>,
    afast::Data(req): afast::Data<MyRequest>,
) -> afast::Result<MyResponse> {
    let db = state.db.lock().await;
    Ok(MyResponse { message: format!("Hello, {}!", req.name) })
}
}

带认证的二进制 Handler(Custom)

#![allow(unused)]
fn main() {
use afast::{AFastDeserialize, AFastSerialize, Tag, handler};

#[derive(AFastDeserialize, Tag)]
#[tag("认证令牌")]
pub struct AuthCustom {
    #[tag("Bearer token")]
    pub token: String,
}

#[handler(desc("需要认证的接口"))]
pub async fn protected(
    afast::State(state): afast::State<AppState>,
    afast::Custom(auth): afast::Custom<AuthCustom>,
    afast::Data(req): afast::Data<MyRequest>,
) -> afast::Result<MyResponse> {
    // auth.token 可用
    Ok(MyResponse { message: "已授权".into() })
}
}

带请求上下文的 Handler(Ctx)

#![allow(unused)]
fn main() {
use afast::{Tag, handler};

#[derive(Clone, Debug)]
pub struct RequestInfo {
    pub request_id: String,
}

#[handler(desc("带上下文的接口"))]
pub async fn with_context(
    afast::Ctx(ctx): afast::Ctx<RequestInfo>,
    afast::State(state): afast::State<AppState>,
) -> afast::Result<MyResponse> {
    println!("请求 ID: {}", ctx.request_id);
    Ok(MyResponse { message: "完成".into() })
}
}

HTTP REST Handler(Ordinary HTTP)

HTTP handler 使用 Header<T> 做认证,返回 HttpResult<Json<T>>

#![allow(unused)]
fn main() {
use afast::{get, post, put, delete, Tag};
use serde::{Deserialize, Serialize};

// HTTP 认证用 Header,不用 Custom
#[derive(Debug, Deserialize, Tag)]
#[tag("HTTP 认证头")]
pub struct AuthHeader {
    #[tag("Authorization 头")]
    pub authorization: String,
}

impl AuthHeader {
    pub fn token(&self) -> &str {
        self.authorization.strip_prefix("Bearer ").unwrap_or(&self.authorization)
    }
}

#[derive(Debug, Deserialize, Tag)]
#[tag("查询参数")]
pub struct ListUsersQuery {
    #[tag("页码")]
    pub page: Option<i64>,
    #[tag("每页数量")]
    pub size: Option<i64>,
}

#[derive(Debug, Serialize, Tag)]
#[tag("用户 HTTP 响应")]
pub struct UserHttp {
    #[tag("用户 ID")]
    pub id: i64,
    #[tag("用户名")]
    pub username: String,
}

#[derive(Debug, Serialize, Tag)]
#[tag("用户列表响应")]
pub struct ListUsersHttpResponse {
    #[tag("总数")]
    pub total: i64,
    #[tag("用户列表")]
    pub items: Vec<UserHttp>,
}

// 注意:#[get] 里是 desc,不是路径!路径在 service! 宏里定义
#[get(desc("通过 HTTP 列出用户"))]
pub async fn list_users_http(
    afast::State(state): afast::State<AppState>,
    afast::Header(auth): afast::Header<AuthHeader>,
    afast::Query(query): afast::Query<ListUsersQuery>,
) -> afast::HttpResult<afast::Json<ListUsersHttpResponse>> {
    let db = state.db.lock().await;
    let _user_id = db.get_user_id_by_token(auth.token()).await
        .ok_or_else(|| afast::Error::custom(401, "invalid token"))?;
    let items = db.read(0, 100).await;
    Ok(afast::Json(ListUsersHttpResponse { total: items.len() as i64, items: vec![] }))
}
}

服务注册

路径在 service! 宏中定义,不在 handler 属性中:

use afast::{AFast, service};

// 二进制 handler 用 h() 包裹
// HTTP handler 路径在 service! 中定义
let admin_svc = service!("admin", "管理服务" => {
    group("user" => {
        // 二进制协议 handler
        h(create_user),
        h(list_users),
        // HTTP REST handler — 路径在这里定义
        get("", list_users_http),
        post("", create_user_http),
        group(":user_id" => {
            get("", get_user_http),
            put("", update_user_http),
            delete("", delete_user_http),
        })
    })
});

// catch-all 路由
let check_svc = service!("check", "检查服务" => {
    h(health),
    get("*", catch_all_get),
});

#[tokio::main]
async fn main() {
    AFast::new()
        .state(AppState::new())
        .service(admin_svc)
        .service(check_svc)
        .http("0.0.0.0:5000")
        .run()
        .await
        .unwrap();
}

AppState 模式

#![allow(unused)]
fn main() {
use std::sync::Arc;
use tokio::sync::Mutex;

#[derive(Clone)]
pub struct AppState {
    pub db: Arc<Mutex<Database>>,
}

impl AppState {
    pub fn new() -> Self {
        Self { db: Arc::new(Mutex::new(Database::new())) }
    }
}
}

类型派生规则

二进制协议类型 — 用于 #[handler]Data<T> 和返回值:

#![allow(unused)]
fn main() {
#[derive(AFastDeserialize, Tag)]
#[tag("请求描述")]
pub struct MyRequest {
    #[tag("字段描述")]
    pub field: String,
}

#[derive(AFastSerialize, Tag)]
#[tag("响应描述")]
pub struct MyResponse {
    #[tag("字段描述")]
    pub field: String,
}
}

HTTP 类型 — 用于 #[get]/#[post] 等的 Body<T>Query<T> 和返回值(需要 serde):

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Tag)]
#[tag("HTTP 请求体")]
pub struct MyBody {
    #[tag("字段描述")]
    pub field: String,
}

#[derive(Debug, Serialize, Tag)]
#[tag("HTTP 响应")]
pub struct MyHttpResponse {
    #[tag("字段描述")]
    pub field: String,
}
}

枚举类型 — 生成的 TypeScript 为 tagged union:

#![allow(unused)]
fn main() {
#[derive(AFastSerialize, AFastDeserialize, Tag)]
#[tag("用户角色")]
pub enum Role {
    #[tag("管理员")]
    Admin,
    #[tag("普通用户")]
    User,
    #[tag("访客")]
    Guest,
}
// 生成的 TS: type Role = { tag: 'Admin', data: null } | { tag: 'User', data: null } | ...
}

错误处理

使用 afast::Error::custom(code, message) 返回自定义错误:

#![allow(unused)]
fn main() {
#[handler(desc("带错误处理的 handler"))]
pub async fn with_error(
    afast::State(state): afast::State<AppState>,
) -> afast::Result<MyResponse> {
    if some_condition {
        return Err(afast::Error::custom(400, "请求参数错误"));
    }
    
    let data = state.db.lock().await.query().await
        .map_err(|e| afast::Error::custom(500, e.to_string()))?;
    
    Ok(MyResponse { message: "成功".into() })
}
}

HTTP 方法(RESTful 路由)

HTTP handler 与二进制 handler 的关键区别:

  • 认证用 Header<T> 而不是 Custom<T>
  • 返回 afast::HttpResult<afast::Json<T>> 而不是 afast::Result<T>
  • 类型需要 serde::Deserialize/serde::Serialize + Tag
  • 路径在 service! 宏中定义,不在 #[get] 属性中
#![allow(unused)]
fn main() {
use afast::{get, post, put, delete, Tag};
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Tag)]
#[tag("查询参数")]
pub struct ListUsersQuery {
    #[tag("页码")]
    pub page: Option<i64>,
    #[tag("每页数量")]
    pub size: Option<i64>,
}

#[derive(Debug, Deserialize, Tag)]
#[tag("创建用户请求")]
pub struct CreateUserBody {
    #[tag("用户名")]
    pub username: String,
    #[tag("密码")]
    pub password: String,
    #[tag("显示名")]
    pub name: String,
}

#[derive(Debug, Deserialize, Tag)]
#[tag("用户 ID 路径参数")]
pub struct UserIdParam {
    #[tag("用户 ID")]
    pub user_id: i64,
}

#[derive(Debug, Deserialize, Tag)]
#[tag("更新用户请求")]
pub struct UpdateUserBody {
    #[tag("显示名")]
    pub name: String,
    #[tag("年龄")]
    pub age: i32,
    #[tag("是否激活")]
    pub active: bool,
}

// 路径在 service! 宏中定义,这里只写 desc
#[get(desc("列出用户"))]
pub async fn list_users_http(
    afast::State(state): afast::State<AppState>,
    afast::Header(auth): afast::Header<AuthHeader>,
    afast::Query(query): afast::Query<ListUsersQuery>,
) -> afast::HttpResult<afast::Json<ListUsersHttpResponse>> {
    // GET /user?page=1&size=10
    Ok(afast::Json(ListUsersHttpResponse { total: 0, items: vec![] }))
}

#[post(desc("创建用户"))]
pub async fn create_user_http(
    afast::State(state): afast::State<AppState>,
    afast::Header(auth): afast::Header<AuthHeader>,
    afast::Body(body): afast::Body<CreateUserBody>,
) -> afast::HttpResult<afast::Json<CreateUserHttpResponse>> {
    // POST /user 带 JSON 请求体
    Ok(afast::Json(CreateUserHttpResponse { id: 1 }))
}

// 在 service! 中的注册方式:
// service!("admin", "管理服务" => {
//     group("user" => {
//         get("", list_users_http),           // GET /user
//         post("", create_user_http),         // POST /user
//         group(":user_id" => {
//             get("", get_user_http),         // GET /user/:user_id
//             put("", update_user_http),      // PUT /user/:user_id
//             delete("", delete_user_http),   // DELETE /user/:user_id
//         })
//     })
// })
}

WebSocket 处理器

路径在 service! 宏中定义:

#![allow(unused)]
fn main() {
use afast::ws;
use afast::extractors::{WsSender, WsReceiver};

#[ws(desc("聊天 WebSocket"))]
pub async fn chat_ws(
    afast::State(state): afast::State<AppState>,
    sender: WsSender,
    receiver: WsReceiver,
) {
    while let Some(msg) = receiver.recv().await {
        sender.send(msg).await;
    }
}

// 在 service! 中注册:
// let chat_svc = service!("chat", "聊天服务" => {
//     ws("/chat/:room", chat_ws),
// });
}

SSE(服务器推送事件)

路径在 service! 宏中定义:

#![allow(unused)]
fn main() {
use afast::sse;
use afast::extractors::SseSender;

#[sse(desc("事件流"))]
pub async fn sse_stream(sender: SseSender) {
    for i in 0..10 {
        sender.send(format!("事件 {}", i)).await;
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    }
}

// 在 service! 中注册:
// let chat_svc = service!("chat", "聊天服务" => {
//     sse("/sse", sse_stream),
// });
}

生命周期钩子

#![allow(unused)]
fn main() {
use afast::hook::{Hook, RequestContext, RequestGuard, ConnectionGuard};

struct MyHook;

impl Hook for MyHook {
    fn before_request(&self, ctx: &RequestContext) -> Option<Box<dyn RequestGuard>> {
        eprintln!("→ {} ({})", ctx.handler_name, ctx.transport);
        Some(Box::new(MyTimer(std::time::Instant::now())))
    }
    
    fn on_connect(&self, ctx: &RequestContext) -> Option<Box<dyn ConnectionGuard>> {
        eprintln!("↕ connect: {}", ctx.handler_name);
        Some(Box::new(MyConnGuard))
    }
}

struct MyTimer(std::time::Instant);
struct MyConnGuard;

impl RequestGuard for MyTimer {
    fn on_response(&mut self, ctx: &RequestContext, _resp: &[u8]) {
        eprintln!("← {} OK ({:?})", ctx.handler_name, self.0.elapsed());
    }
    fn on_error(&mut self, ctx: &RequestContext, err: &afast::Error) {
        eprintln!("✗ {} error: {}", ctx.handler_name, err);
    }
}

impl ConnectionGuard for MyConnGuard {
    fn on_disconnect(&mut self, ctx: &RequestContext) {
        eprintln!("✕ disconnect: {}", ctx.handler_name);
    }
}

// 在 main 中注册:
// AFast::new().hook(MyHook)
}

速率限制

#[handler] 属性中使用 rate_limit("policy_name"),并在 main 中配置策略:

#![allow(unused)]
fn main() {
use afast::RateLimitConfig;

#[handler(desc("带限流的接口"), rate_limit("api_limit"))]
pub async fn limited_endpoint(
    afast::State(state): afast::State<AppState>,
) -> afast::Result<MyResponse> {
    Ok(MyResponse { message: "成功".into() })
}

// 在 main 中配置限流策略:
// AFast::new()
//     .rate_limit(RateLimitConfig::new()
//         .policy("api_limit", afast::RateLimitPolicy::fixed_window(100, 60)))
}

客户端代码生成

启动服务后,可以生成客户端代码:

# TypeScript
curl http://localhost:5000/code/auth/ts

# JavaScript
curl http://localhost:5000/code/auth/js

# Kotlin
curl http://localhost:5000/code/auth/kt

# Rust
curl http://localhost:5000/code/auth/rs

其中 auth 是 service 名称。每个 service 生成一个独立的客户端文件。

客户端使用示例

TypeScript / JavaScript 客户端

生成的 TS 客户端为每个 service 创建一个 Client 类,通过 apis 属性暴露所有 handler 方法。

import { AuthClient } from './auth';
import { AdminClient } from './admin';

// ── 二进制协议客户端(fetch 模式,最常用)──
const auth = new AuthClient({
    host: 'localhost',
    port: 5001,
    tls: false,
    transport: 'fetch',  // 'fetch' | 'ws' | 'nodetcp' | 'buntcp'
    debug: true,         // 开启后会打印请求/响应日志
    customs: {
        // Custom<T> 提取器需要提供一个返回 Promise 的工厂函数
        AuthCustom: async () => ({ token: myToken })
    }
});
await auth.apis._ready;  // 等待连接就绪(fetch 模式立即就绪)

// 调用二进制 handler — 参数和返回值都有完整类型
const reg = await auth.apis.signup({
    username: 'alice',
    password: 'secret',
    name: 'Alice'
});
console.log(reg.user.username);  // 'alice'
console.log(reg.token);          // 自动生成的 token

const login = await auth.apis.login({
    username: 'alice',
    password: 'secret'
});

// 无参数的 handler
const uid = await auth.apis.get_user_id();

// ── 同时有二进制和 HTTP 路由的客户端 ──
const admin = new AdminClient({
    host: 'localhost',
    port: 5001,
    tls: false,
    transport: 'fetch',
    customs: {
        AuthCustom: async () => ({ token: myToken })
    },
    headers: {
        // HTTP 路由的 Header<T> 认证
        AuthHeader: async () => ({ authorization: `Bearer ${myToken}` })
    }
});
await admin.apis._ready;

// 二进制 handler — 通过 apis 下的分组访问
const users = await admin.apis.user.list_users({ page: 1, size: 10 });
console.log(users.total, users.items);

const newUser = await admin.apis.user.create_user({
    username: 'bob', password: 'pw', name: 'Bob'
});

// HTTP REST handler — 通过 apis 下的分组访问
const httpUsers = await admin.apis.user.list_users_http({
    queries: { page: 1, size: 10 }  // Query 参数放在 queries 中
});

const created = await admin.apis.user.create_user_http({
    body: { username: 'charlie', password: 'pw', name: 'Charlie' }  // Body 放在 body 中
});

// 路径参数自动从参数对象中提取
const updated = await admin.apis.user.user_id.update_user_http({
    user_id: 123,  // 路径参数 :user_id
    body: { name: 'Updated', age: 25, active: true }
});

// ── WebSocket 模式(支持长连接和推送)──
const wsAuth = new AuthClient({
    host: 'localhost',
    port: 3001,
    tls: false,
    transport: 'ws',
    customs: { AuthCustom: async () => ({ token: myToken }) }
});
await wsAuth.apis._ready;
// 使用方式完全相同,底层自动走 WebSocket 二进制帧
const result = await wsAuth.apis.login({ username: 'alice', password: 'secret' });

Kotlin 客户端

import afast.generated.*

fun main() = runBlocking {
    // 创建客户端
    val auth = AuthClient(
        host = "localhost",
        port = 5001,
        tls = false,
        transport = "http",  // "http" | "ws" | "tcp"
        customFns = AuthCustomFns(
            AuthCustom = { AuthCustom(token = myToken) }
        )
    )

    // 调用 handler — suspend 函数,直接在协程中调用
    val reg = auth.apis.signup(RegisterRequest(
        username = "alice",
        password = "secret",
        name = "Alice"
    ))
    println(reg.user.username)  // "alice"
    println(reg.token)

    val login = auth.apis.login(LoginRequest(
        username = "alice",
        password = "secret"
    ))

    // 无参数
    val uid = auth.apis.get_user_id()

    // 带 HTTP 路由的 service
    val admin = AdminClient(
        host = "localhost",
        port = 5001,
        tls = false,
        transport = "http",
        customFns = AdminCustomFns(
            AuthCustom = { AuthCustom(token = myToken) }
        ),
        headerFns = AdminHeaderFns(
            AuthHeader = { AuthHeader(authorization = "Bearer $myToken") }
        )
    )

    // 二进制 handler
    val users = admin.apis.user.list_users(ListUsersRequest(page = 1, size = 10))

    // HTTP REST handler
    val httpUsers = admin.apis.user.list_users_http(
        queries = UserListUsersHttpQuery(page = 1, size = 10)
    )
}

客户端类型自动生成规则

Rust 类型TypeScript 类型说明
Stringstring
i32/i64/u32/u64/f32/f64number
boolboolean
Vec<T>T[]
Option<T>T | null
HashMap<K,V>Record<K,V>
Vec<u8>Uint8Array
enum { A, B }{ tag: 'A', data: null } | { tag: 'B', data: null }Tagged union
#[handler(name("signup"))]apis.signup(...)客户端方法名
group("user" => { ... })apis.user.xxx(...)嵌套分组

客户端支持的传输方式

传输方式说明适用场景
fetchHTTP/1.1 或 HTTP/2浏览器、Node.js(最常用)
wsWebSocket 二进制帧浏览器、Node.js 长连接
nodetcpNode.js TCPNode.js 高性能场景
buntcpBun TCPBun 运行时
unirequestuni-app HTTP小程序/APP
uniwsuni-app WebSocket小程序/APP 长连接
wxrequest微信小程序 HTTP微信小程序
wxws微信小程序 WebSocket微信小程序长连接

常见错误

  1. 忘记 Tag 派生 — 所有在 handler 中使用的类型必须派生 Tag,字段需要 #[tag("描述")]
  2. 不要用 String 接收请求体 — 始终使用 Data<T> 配合结构体
  3. 忘记 Result 返回类型 — 二进制 handler 返回 afast::Result<T>,HTTP handler 返回 afast::HttpResult<afast::Json<T>>
  4. 不要手动注册路由 — 使用 #[handler]
  5. 不要 clone StateState<T> 持有 &'static T,直接使用即可
  6. 参数必须解构 — 写 afast::State(state): afast::State<AppState> 而不是 state: State<AppState>
  7. HTTP 认证用 Header — HTTP handler 用 Header<T> 做认证,不要用 Custom<T>
  8. HTTP 类型需要 serde — HTTP handler 的请求/响应类型需要 Deserialize/Serialize + Tag
  9. #[get] 里写 desc 不写路径 — 路径在 service! 宏中定义
  10. 路径参数在 service! 中用 :param — 如 group(":user_id" => { get("", handler) })

项目结构模板

my-project/
├── Cargo.toml
└── src/
    ├── main.rs          # 入口,service! 定义和 AFast 配置
    ├── state.rs         # AppState + Database 定义
    └── handler/
        ├── mod.rs
        ├── auth.rs      # 认证相关 handler
        ├── admin.rs     # 管理相关 handler(含 HTTP 路由)
        └── chat.rs      # WebSocket/SSE handler

快速参考

用途示例
#[handler]定义二进制协议 handler#[handler(desc("..."), name("..."), cache(60), rate_limit("..."))]
#[get]定义 HTTP GET#[get(desc("..."))] — 路径在 service! 中
#[post]定义 HTTP POST#[post(desc("..."))]
#[put]定义 HTTP PUT#[put(desc("..."))]
#[delete]定义 HTTP DELETE#[delete(desc("..."))]
#[ws]定义 WebSocket#[ws(desc("..."))]
#[sse]定义 SSE#[sse(desc("..."))]
service!创建服务并定义路由service!("name", "desc" => { h(fn), get("path", fn) })
h()注册二进制 handlerh(my_handler)
group()路由分组group("user" => { get("", fn), group(":id" => { get("", fn) }) })
提取器用途解构语法
State<T>共享应用状态afast::State(state): afast::State<AppState>
Data<T>二进制请求体afast::Data(req): afast::Data<MyRequest>
Custom<T>二进制认证上下文afast::Custom(auth): afast::Custom<AuthCustom>
Ctx<T>请求上下文(hook 写入)afast::Ctx(ctx): afast::Ctx<RequestInfo>
Query<T>HTTP 查询参数afast::Query(q): afast::Query<MyQuery>
Param<T>HTTP 路径参数afast::Param(p): afast::Param<MyParam>
Body<T>HTTP 请求体afast::Body(b): afast::Body<MyBody>
Header<T>HTTP 请求头afast::Header(h): afast::Header<AuthHeader>
已复制到剪贴板!