1. Web Server
学完这章,你能用 Prolog 搭建一个带数据库的 REST API,甚至可以部署到生产环境。 前置:模块化 | 难度:★★ | 后续:—
Prolog 还能写 web server?我第一次听说时也是这个反应。但你想想——Prolog 能做逻辑推理、能查数据库、能解析 JSON,凭什么不能处理 HTTP 请求?
SWI-Prolog 内置了一套完整的 HTTP 库(library(http/*)),支持路由、JSON、Session、WebSocket。对内部工具或原型开发,比另起一个 Node.js 或 Flask 还省事——数据库、业务逻辑、HTTP 全在一个语言里,不用在项目之间切上下文。
学完这章,你能用 Prolog 写一个带 JSON API 和 SQLite 数据库的 Web 服务,部署到服务器上跑起来。
1.1. Hello Web —— 三行代码起服务
先来看最简版本。创建一个 websev.pl:
:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).
:- use_module(library(http/http_unix_daemon)).
:- http_handler(/, handle_request, []).
handle_request(_Request) :-
format("Content-type: text/plain~n~n"),
format("Hello!").
启动它:
?- http_server(handle_request, [port(8080)]).
% HTTP server started on port 8080
打开浏览器访问 http://localhost:8080/——看到 Hello! 了吧。
是不是比想象中简单?三个 use_module 加载 HTTP 库,一个 http_handler 注册路由,一个 handle_request 处理请求。没了。
1.2. http_handler 和 http_server —— 核心搭档
1.2.1. http_server/2 —— 启动服务器
http_server(:Handle, +Options).
它把 Prolog 进程变成一个 HTTP 服务器。Handle 通常是 http_dispatch(让 http_handler 自动分发),Options 最常用:
| 选项 | 作用 | 默认值 |
|---|---|---|
port(Port) |
端口号 | 4000 |
workers(N) |
工作线程数 | 4 |
timeout(Secs) |
连接超时 | 无 |
1.2.2. http_handler/3 —— 注册路由
http_handler(+Path, :Handle, +Options).
路由规则很直观:
:- http_handler(/, root_handler, []).
:- http_handler(/api/users, users_handler, []).
:- http_handler(/files/, file_handler, [prefix]).
[prefix] 表示前缀匹配——/files/foo.txt 也会路由到 file_handler。
1.2.3. Request 对象
Handler 收到的 Request 是一个 Prolog 列表,包含请求的所有信息:
[ method(Method)
, path(Path)
, search_params(Params)
, ...
]
提取参数用 http_parameters/2:
handle_request(Request) :-
http_parameters(Request, [name(Name), age(Age, [number])]),
format("Name=~w, Age=~d~n", [Name, Age]).
跨语言类比:
http_parameters/2就像 Express 的req.query或 Flask 的request.args。但 Prolog 的版本自带类型标注(比如[number])。
1.3. 实战:JSON REST API
来写个真正的 API——一个支持 GET/POST 的小服务。
:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).
:- use_module(library(http/http_json)).
:- use_module(library(http/json)).
:- http_handler(/, root, []).
:- http_handler(/api/echo, echo_handler, []).
:- http_handler(/api/data, data_handler, []).
server :-
http_server(http_dispatch, [port(8080)]).
%% 根路由:HTML 欢迎页
root(_Request) :-
reply_html_page(title("Prolog Web"),
p("Prolog Web Server Running!")).
%% POST /api/echo — 接收 JSON,原样返回
echo_handler(Request) :-
http_read_json_dict(Request, Dict),
Reply = _{ status: ok, echo: Dict.get(data) },
reply_json_dict(Reply).
%% GET /api/data — 返回结构化 JSON
data_handler(_Request) :-
Data = _{ items: [_{id: 1, name: "foo"}, _{id: 2, name: "bar"}]
, total: 2
, timestamp: 1_700_000_000
},
reply_json_dict(Data, [status(200)]).
:- initialization(server).
保存为 webserver.pl,运行:
$ swipl -q webserver.pl
测试:
$ curl http://localhost:8080/
$ curl http://localhost:8080/api/data
$ curl -X POST -H "Content-Type: application/json" \
-d '{"data":"hello"}' http://localhost:8080/api/echo
看到返回的 JSON 了吧。reply_json_dict/1 自动设置 Content-Type: application/json,http_read_json_dict/2 自动解析请求体。来回都是 Prolog dict,比在其他语言里手动序列化省事多了。
Prolog 时刻:
reply_json_dict/1接受的_{...}语法是 SWI-Prolog 的 dict 字面量。它和 JSON 几乎一一对应——这意味着你在 Prolog 里操作 JSON,本质上操作的就是 Prolog 数据结构,不需要额外的 ORM 或序列化层。
1.4. 文件服务——一行搞定
要托管静态文件?最推荐的方式——一行:
:- use_module(library(http/http_server_files)).
:- http_handler('/static/', http_reply_from_dir('static'), [prefix]).
http_reply_from_dir/2 自动处理 MIME 类型、缓存头、目录遍历防护。比自己手写 http_reply_file 安全得多。
1.5. Middleware 模式
进阶内容,可跳过
我们有了路由、JSON、文件服务。但还缺什么?日志、认证、限流——这些横切关注点不该写在每个 handler 里。
Middleware 模式就是一层层包装 handler:每个 middleware 接收一个 handler,返回加了料的 handler。
1.5.1. logger middleware
middleware_logger(Handler) :-
Handler.
%% 完整实现需要 hook 请求生命周期,这里只是示意
1.5.2. auth middleware
检查 Authorization header 里的 bearer token:
:- use_module(library(http/http_header)).
middleware_auth(Handler) :-
Handler.
%% 实际检查:
%% auth_wrapper(Request, Handler) :-
%% ( http_header(Authorization, Bearer Token, Request)
%% -> Handler(Request)
%% ; reply_json_dict(_{error: unauthorized}, [status(401)])
%% ).
1.5.3. rate_limit middleware
用 nb_setval/2 做简单计数器:
middleware_rate_limit(Handler) :-
Handler.
%% 示例:
%% rate_limit_check :-
%% ( nb_current(request_count, Count)
%% -> NewCount is Count + 1
%% ; NewCount = 1
%% ),
%% nb_setval(request_count, NewCount),
%% ( NewCount > 100
%% -> reply_json_dict(_{error: rate_limit_exceeded}, [status(429)])
%% ; true
%% ).
1.5.4. 组合 middleware
通过嵌套组合:
compose_middleware([], Handler) :- Handler.
compose_middleware([MW|Rest], Handler) :-
compose_middleware(Rest, Inner),
call(MW, Inner).
%% 使用
:- http_handler(/, compose_middleware([middleware_logger, middleware_auth, middleware_rate_limit], handle_request), []).
1.5.5. 局限
Prolog 没有函数式语言的 middleware 链天然表达力,上述是概念演示。生产环境建议用 library(http/http_hook) 或 thread-local 变量。
1.6. 数据库操作(SQLite)
进阶内容,可跳过
API 有了,数据放哪?SWI-Prolog 通过 library(sqlite) 提供 SQLite 绑定。
1.6.1. 连接与建表
:- use_module(library(sqlite)).
setup_db :-
sqlite_open('todos.db', DB, []),
sqlite_exec(DB, 'CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task TEXT NOT NULL,
done INTEGER DEFAULT 0
)'),
sqlite_close(DB).
1.6.2. CRUD 操作
%% 创建 TODO
add_todo(DB, Task) :-
sqlite_exec(DB, 'INSERT INTO todos (task) VALUES (?)', Task).
%% 查询所有 TODO
list_todos(DB, Todos) :-
findall(todo{id:Id, task:Task, done:Done},
( sqlite_query(DB, 'SELECT id, task, done FROM todos', row(Id, Task, Done))
), Todos).
%% 标记完成
toggle_todo(DB, Id) :-
sqlite_exec(DB, 'UPDATE todos SET done = CASE WHEN done THEN 0 ELSE 1 END WHERE id = ?', Id).
%% 删除 TODO
delete_todo(DB, Id) :-
sqlite_exec(DB, 'DELETE FROM todos WHERE id = ?', Id).
1.6.3. 集成到 Web 服务器
把 SQLite 和 HTTP 连起来——一个 Todo API:
:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).
:- use_module(library(http/http_json)).
:- use_module(library(sqlite)).
:- use_module(library(http/http_header)).
:- sqlite_open('todos.db', DB, []).
:- http_handler(/api/todos, todos_handler, []).
todos_handler(Request) :-
http_parameters(Request, [method(M)]),
( M = get
-> list_todos(DB, Todos),
reply_json_dict(_{todos: Todos})
; http_read_json_dict(Request, Body),
add_todo(DB, Body.task),
reply_json_dict(_{status: ok})
).
Prolog 时刻:注意
add_todo(DB, Task)里的?占位符——sqlite_exec/3自动处理参数化查询,防 SQL 注入。Prolog 的变量绑定天然适合做 prepared statement。
1.6.4. 注意
sqlite_exec/3参数化查询用?占位sqlite_query/3返回row(...);用findall/3+ dict 方便 JSON 序列化- 生产环境要管理连接池或每次请求独立连接
1.7. 生产考量
进阶内容,可跳过
开发环境跑通了,上线呢?
1.7.1. CORS 设置
浏览器跨域请求需要返回 CORS headers:
:- use_module(library(http/http_header)).
cors_handler(Handler) :- Handler.
%% cors_set_headers :-
%% http_set_response_header(access_control_allow_origin, '*'),
%% http_set_response_header(access_control_allow_methods, 'GET, POST, PUT, DELETE, OPTIONS'),
%% http_set_response_header(access_control_allow_headers, 'Content-Type, Authorization').
1.7.2. Session 管理
:- use_module(library(http/session_manager)).
%% 配置 session
:- initialization(http_session_manager(expire(3600))).
%% 在 handler 中使用
session_handler(Request) :-
http_open_session(Request),
http_session_data(user(User)),
( var(User)
-> http_session_assert(user(anonymous))
; true
).
1.7.3. Request size limit
http_server(http_dispatch,
[ port(8080)
, limit(10_000_000) %% 请求体上限 10MB
]).
1.7.4. 并发安全
SWI-Prolog HTTP 服务器基于多线程(workers(N) 控制线程数)。注意:
nb_setval/2非线程安全- 线程间共享数据用
thread_local+with_mutex/2 library(sqlite)实例默认线程安全,但多线程写需同步
:- thread_local request_counter/1.
increment_counter :-
with_mutex(counter,
( request_counter(C),
retractall(request_counter(_)),
NC is C + 1,
asserta(request_counter(NC))
; asserta(request_counter(1))
)).
1.7.5. 部署建议
方式一:daemon 模式
:- use_module(library(http/http_unix_daemon)).
:- initialization(http_daemon).
http_daemon 自动 fork 到后台、写 PID 文件、处理日志。
方式二:nginx 反向代理
# nginx 配置片段
upstream prolog_backend {
server 127.0.0.1:8080;
}
server {
listen 80;
location / {
proxy_pass http://prolog_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
nginx 处理 TLS、静态文件、负载均衡,Prolog 专心做业务逻辑。
1.7.6. 其他推荐
library(http/http_log)记录访问日志library(http/http_error)自定义错误页面library(http/http_monitor)监控
1.8. 总结
SWI-Prolog 的 HTTP 库比你想的成熟得多。一个 .pl 文件就能搞定全套——路由、JSON、数据库、HTTPS、daemon。
| 你要做的事 | 用什么 |
|---|---|
| 启动服务器 | http_server/2 |
| 注册路由 | http_handler/3 |
| 返回 JSON | reply_json_dict/1 |
| 解析请求 JSON | http_read_json_dict/2 |
| 静态文件 | http_reply_from_dir/2 |
| 数据库 | library(sqlite) |
| 后台运行 | http_daemon |
最适合的场景:内部 API、原型、小工具。下次想快速搭一个 API,试试 Prolog——你可能会有惊喜。
参考:The Power of Prolog - Web Applications — SWI-Prolog Web 库官方示例。 | library(sqlite)