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/jsonhttp_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)

Copyright © zhzluke96 2020 all right reserved,powered by Gitbook该文件修订时间: 2026-06-30 16:08

results matching ""

    No results matching ""