# HTTP 服务器 Deft 的 HTTP 服务器构建在 TCP 原语之上,运行在同一个 epoll 事件循环上。 每个请求由一个协程处理,因此处理器可以 `await` 异步操作(数据库查询、 上游 HTTP 调用、定时器)而不阻塞其他连接。 路由器由标准映射经 `http/router` 包装而成:
def app %{...} |> [http/router]
## 你好,服务器
def app %{
    routes @{
        %{ path "/" method :GET handler { "Hello, World!" } }
    }
} |> [http/router]

http/serve $app :8080
用 `deft server.dft` 运行,然后 `curl http://localhost:8080/`。 ## 路由器构建 路由器是一个带有以下可选键的映射:
def app %{
    prefix "/api"                                  # optional path prefix
    routes @{
        %{ path "/"            method :GET  handler root-handler }
        %{ path "/users/:id"   method :GET  handler get-user }
        %{ path "/users"       method :POST handler create-user }
        %{ path "/files/*rest" method :GET  handler get-file }
    }
    statics @{
        %{ path "/assets" dir "./public" }
    }
    middleware @{
        {req res} { echo "request: $req~method $req~path" }
    }
} |> [http/router]
### `routes` 路由映射的列表。每条路由包含: - `path` —— URL 模式。`:name` 将路径段捕获到 `$req~params~name`。 `*name` 捕获路径的其余部分(零个或多个段)。 - `method` —— `:GET`、`:POST`、`:PUT`、`:PATCH`、`:DELETE`、`:HEAD`。 - `handler` —— 函数 `{req res}` 或闭包。 ### `statics` 静态文件目录的列表:
%{ path "/assets" dir "./public" }
`/assets/` 下的请求由 `./public/` 下的文件提供。 开发期间使用 `http/serve` 上的 `--watch` 记录文件系统变更。 ### `prefix` 应用到路由器中每条路由和静态目录的路径前缀:
def api %{
    prefix "/api/v1"
    routes @{
        %{ path "/users" method :GET handler list-users }    # GET /api/v1/users
    }
} |> [http/router]
### `middleware` 处理器函数列表,按顺序在匹配的路由之前调用。 中间件接收 `{req res}`,并可以通过写出响应来短路。 ## 请求与响应 处理器接收 `%Request{}` 和 `%Response{}` 标记映射。
def get-user {|req res|
    @doc "Fetch a user by id."
    set id $req~params~id                    # captured from :id in the path
    set user [load-user-from-db $id]
    if $user {
        http/json $res $user
    } else {
        http/status $res 404
        http/html $res "not found"
    }
}
### 请求字段 | 字段 | 类型 | 说明 | |---|---|---| | `:method` | 关键字 | `:GET`、`:POST` 等 | | `:path` | 字符串 | URL 路径 | | `:headers` | 映射 | 小写的请求头名称 → 值 | | `:params` | 映射 | 路径捕获(`:id` → `$req~params~id`) | | `:query` | 映射 | 解析后的查询字符串 | | `:body` | 字符串 | 请求体 | ### 响应辅助函数 | 函数 | 用途 | |---|---| | `[http/respond $res $body $content-type]` | 写出响应体 | | `[http/json $res $data]` | JSON 响应(自动编码) | | `[http/html $res $html]` | HTML 响应 | | `[http/status $res $code]` | 设置状态码 | | `[http/header $res "name" "value"]` | 设置响应头 | | `[http/redirect $res "/url" ?code]` | 重定向(默认 302) | ## 隐式响应与显式响应 发送响应的两种方式: 隐式 —— 从处理器返回响应体:
%{ path "/" method :GET handler { "Hello, World!" } }
直接返回字符串会变成 200 OK,字符串作为响应体。 显式 —— 通过响应辅助函数写出:
def get-user {|req res|
    http/status $res 200
    http/header $res "Content-Type" "application/json"
    http/respond $res [json/stringify $user] "application/json"
}
当需要设置状态码、响应头或特定内容类型时,请使用显式方式。 ## 异步处理器 处理器作为协程在事件循环上运行。每个请求都会派生一个协程; 连接会保持存活直到协程完成。对任何异步操作都使用 `await`:
def proxy {|req res|
    @doc "Proxy to an upstream service."
    set backend [await [http/get [str "http://127.0.0.1:3001" $req~path]]]
    http/status $res $backend~status
    http/respond $res $backend~body "application/json"
}

def api %{
    routes @{ %{ path "/api/*rest" method :GET handler $proxy } }
} |> [http/router]
def slow {|req res|
    @doc "Sleep 2 seconds, non-blocking for other connections."
    sleep 2000
    http/html $res "<h1>done</h1>"
}
同步处理器的行为完全相同 —— 它们在单个事件循环 tick 内运行至完成。 ## 长连接 默认情况下连接在每次响应后关闭。要启用 HTTP/1.1 长连接:
http/header $res "Connection" "keep-alive"
## HTML 模板 `[html ... \html]` 块生成 HTML 字符串。完整的模板系统 —— 插值形式、 转义、局部模板、定义自己的模板块,以及 `[sql ... \sql]` —— 记录在 模板 中。
def render-user {|user|
    [html
        <div class="user">
            <h1>$user~name</h1>
            <p>Age: $user~age</p>
        </div>
    \html]
}

def handler {|req res|
    http/html $res [render-user $alice]
}
## 静态文件
def app %{
    statics @{
        %{ path "/assets"   dir "./public" }
        %{ path "/downloads" dir "/var/files" }
    }
    routes @{
        %{ path "/" method :GET handler { [unwrap [fs/read "./public/index.html"]] } }
    }
} |> [http/router]
添加 `--watch` 以记录变更:
http/serve $app :8080 --watch
## 中间件 中间件按顺序在匹配的路由之前触发。每个中间件函数接收 `{req res}`:
def logger {|req res|
    @doc "Log every request."
    echo "$req~method $req~path"
}

def auth {|req res|
    @doc "Require an Authorization header."
    if ![get $req~headers "authorization"] {
        http/status $res 401
        http/html $res "auth required"
        return
    }
}

def api %{
    middleware @{ $logger $auth }
    routes @{ %{ path "/secure" method :GET handler { "secret" } } }
} |> [http/router]
### 异步中间件 中间件与路由处理器运行在相同的协程机制上,因此其中可以使用 `[await ...]`。等待数据库查询的中间件只会挂起自己的请求;其他连接 继续运行:
def load-user {|req res|
    @doc "Resolve the Authorization token to a user before the handler runs."
    def token [get $req~headers "authorization"]
    if token {
        set user [await [sqlite/query $db "SELECT * FROM users WHERE token = ?" @{ $token }]]
        if ![empty? $user] {
            [http/header $res "X-User" $user~first~name]
        }
    }
}

def api %{
    middleware @{ $load-user }
    routes @{ %{ path "/me" method :GET handler { "profile" } } }
} |> [http/router]
返回 `false` 的中间件会拒绝请求 —— 响应状态按原样发送,下游中间件和 路由处理器都不会运行。提前返回(在写出响应之后)的中间件效果相同: 它会阻止下游中间件和路由处理器运行。 ## 多分派模式 对于不太简单的处理器,将逻辑与 HTTP 适配层分开定义:
# Pure logic — testable without HTTP
def load-user {|id^string|
    [db-query "SELECT * FROM users WHERE id = ?" @{ $id }]
}

# HTTP adapter
def get-user {|req res|
    set user [load-user $req~params~id]
    http/json $res $user
}

def api %{
    routes @{ %{ path "/users/:id" method :GET handler $get-user } }
} |> [http/router]
这种模式(逻辑放在 `def` 中,HTTP 形态放在处理器中)让处理器保持 小巧,并让你无需启动服务器即可对业务逻辑进行单元测试。 ## 示例 ### JSON API
def api %{
    prefix "/api/v1"
    routes @{
        %{ path "/users"      method :GET  handler list-users }
        %{ path "/users/:id"  method :GET  handler get-user }
        %{ path "/users"      method :POST handler create-user }
    }
} |> [http/router]

def users-db @{}

def list-users {|req res| http/json $res $users-db }

def get-user {|req res|
    set id $req~params~id
    set u [get $users-db $id]
    if $u {
        http/json $res $u
    } else {
        http/status $res 404
        http/json $res %Err{ :message "not found" }
    }
}

def create-user {|req res|
    set data [json/parse $req~body]
    set id $data~id
    set users-db [assoc $users-db $id $data]
    http/status $res 201
    http/json $res $data
}

http/serve $api :8080
### 带静态文件与回退的 SPA
def app %{
    statics @{
        %{ path "/assets" dir "./dist/assets" }
    }
    routes @{
        %{ path "/*rest" method :GET handler { [unwrap [fs/read "./dist/index.html"]] } }
    }
} |> [http/router]
## 服务器生命周期
def srv [http/serve $app :8080]

# ... later ...

http/stop $srv
`http/stop` 是优雅停止 —— 进行中的请求会在服务器退出前完成。 ## 集群模式 对于生产工作负载,App Server 会在 N 个工作进程中运行同一个应用, 每个进程用 `SO_REUSEPORT` 绑定同一端口 —— 内核在 accept 之间进行 负载均衡,无需应用层代理跳转。这就是 App Server;集群部署、重载和 跨工作进程状态参见 App Server。 ## 快速参考 | 形式 | 用途 | |---|---| | `def name %{ ... } \|> [http/router]` | 构建路由器 | | `[http/serve $router :port]` | 启动服务器 | | `[http/serve $router :port --watch]` | 监视静态目录 | | `[http/stop $server]` | 停止服务器 | | `[http/respond $res $body $content-type]` | 写出响应 | | `[http/json $res $data]` | JSON 响应 | | `[http/html $res $html]` | HTML 响应 | | `[http/status $res $code]` | 设置状态码 | | `[http/header $res "name" "value"]` | 设置响应头 | | `[http/redirect $res "/url" ?code]` | 重定向 | | `[http/sse $res ?opts]` | 打开 Server-Sent Events 流 —— 参见 SSE | | 集群标准库函数 | 参见 App Server |