# 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 |