diff options
| author | eltrac <hi@eltr.ac> | 2026-10-03 21:18:59 +0800 |
|---|---|---|
| committer | eltrac <hi@eltr.ac> | 2026-10-03 21:18:59 +0800 |
| commit | 7b98a8c41a5d16001e8e609f1e12dd336630bb7d (patch) | |
| tree | 9ba9234654e4a3f32a708f690b2f98011ce58d11 | |
| parent | 69d55b7a2498f40daa5b13a2090352db5bd3f7e2 (diff) | |
[draft] new
| -rw-r--r-- | content/article/clj6.md | 165 |
1 files changed, 165 insertions, 0 deletions
diff --git a/content/article/clj6.md b/content/article/clj6.md new file mode 100644 index 00000000..14d4bb3c --- /dev/null +++ b/content/article/clj6.md @@ -0,0 +1,165 @@ ++++ +title = 'Coffee Break Clojure, Vol.6' +subtitle = 'Ring Ring Ring' +slug = 'clj6' +tags = ["Lisp","编程"] +categories = '茶歇蔻芥' +date = 2026-10-03T20:05:00+08:00 +draft = true +toc = true +endnotes = [ + '本文副标题 *Ring Ring Ring*,来自 S.H.E. 的[同名歌曲](https://redirect.invidious.io/watch?v=nnQNiPAK-nY)。' +] ++++ + +[上一篇](/article/clj5/)讲解了如何用 Leiningen 管理 Clojure 项目的目录结构和依赖项,今天我们来了解 Clojure 生态中最常见的 Web 后端库 [Ring](https://github.com/ring-clojure/ring)。不对,和那个 [IndieWeb Ring](https://xn--sr8hvo.ws/) 没有关系。<!--more--> + +## 初识 Ring + +Ring 是对 Web 服务器的**低层**抽象,也就是说没有实现页面路由(router),和通常的后端框架(比如 Go 的 Gin、Python 的 Django、Java 的 Spring Web 等有很大区别。 + +根据 [Ring Spec 1.6.1](https://github.com/ring-clojure/ring/blob/master/SPEC.md),Ring 主要有三个概念: + +1. Handler(处理器) +2. Middleware(中间件) +3. Adapter(适配器) + +Ring 提供了三种 API: 同步 API、异步 API 和 WebSocket API。不同的 API 在概念上略有不同,我们先从最简单的同步 API 讲起。 + +Handler 就是一个函数,接收 HTTP 请求,返回 HTTP 响应。 + +```clojure +;; 一个示例 Handler +(fn [request] response) +``` + +Middleware 接收一个或多个 Handler,也可能接受一些参数,最后返回新的 Handler。可以理解为设计模式中的装饰器模式(decorator pattern),用来调整或增强 Handler 的行为,比如把 Handler 返回的响应体都解析成 JSON 字符串。 + +Adapter 接收一个 Handler 和其他配置参数,在调用时启动 HTTP 服务器。 + +```clojure +;; 调用一个 Adapter 的示例 +(run-adapter handler options) +``` + +接收 HTTP 请求的就是 Adapter,他会把请求包装成 Request Map 传递给 Handler,随后把 Handler 返回的 Response Map 构造成 HTTP 响应发送给客户端。可以把 Adapter 理解为设计模式中的代理模式(proxy pattern),用户不直接调用 Handler,而是通过 Adapter 调用 Handler,Adapter 会处理 HTTP 协议相关的细节。 + + 绘制") +{.dark:invert} + +上述组件其实都不必有代码依赖关系,因为函数可以作为数据传递,上图的虚线都表示数据流。Handler 先是经过 Middleware 被改装成了新的 Handler,然后被传入 Adapter;Adapter 在 Web 服务器有请求进来时调用 Handler 产生响应数据,把响应数据包装成响应体后传送给客户端。 + +## 简单的 Web 服务器 + +先在 `project.clj` 里添加两个新依赖: + +```clojure + :dependencies [[org.clojure/clojure "1.10.1"] + [ring/ring-core "1.8.2"] + [ring/ring-jetty-adapter "1.8.2"]]) +``` + +其中一个是 Ring 的核心库,另一个是 Jetty[^1] 的 Ring Adapter。`ring-core` 也提供了一些 Middleware,我们其实只需要写 Handler。 + +创建 `src/antfigher/handler.clj`,`antfigher` 是我用来演示的项目名。我们先让 Handler 只返回 `Hello!` 这一条信息。 + +```clojure +(ns antfighter.handler) + +(defn handler [request] + {:status 200 + :body "Hello!"}) +``` + +接下来需要启动 Web 服务器,如果你安装了上一篇提到的 `lein-ring` 插件,就可以直接用命令行启动了。在 `project.clj` 中添加这些配置: + +```clojure + :plugins [[lein-ring "0.12.6"]] + :ring {:handler antfighter.handler/handler} +``` + +`:plugins` 安装了 Ring 插件,`:ring` 则是这个插件的配置项,其中 `:handler` 指定了 `antfigher.handler` 命名空间下的 `handler` 符号,也就是我们刚才定义的 `handler` 函数。 + +在终端输入 `lein ring server` 启动服务器,之后工具会自动在浏览器里打开 `http://localhost:3000`,然后就能看到 `Hello!` 显示在浏览器页面里。 + +当然,如果要打包成可以直接在 JVM 上运行的 `.jar` 文件,不自己调用 Adapter 是不行的。在 `core.clj` 文件的 `-main` 函数里编写启动逻辑: + +```clojure {hl_lines=[3,4,9]} +(ns antfighter.core + (:gen-class) + (:require [antfighter.handler :refer [handler]] + [ring.adapter.jetty :refer [run-jetty]])) + +(defn -main + "I start a Jetty server." + [& args] + (run-jetty handler {:port 3000})) +``` + +`run-jetty` 就是 Ring Jetty Adapter,它接收 `handler` 函数和作为配置项的映射,这里我们传入的配置是 `{:port 3000}`,也就是在 `3000` 端口上启动 Jetty 服务器。 + +这时运行 `lein run`,再手动打开 `localhost:3000`,也能看到 `Hello!`。如果用 `lein uberjar` 把项目编译成 `.jar`,然后用 `java -jar xxx.jar` 运行,也是一样的效果。[^2] + +开发时建议用 `lein ring server`,因为自带热重载,保存代码后就能看到效果。 + +## 处理 HTTP 请求 + +Handler 接收的参数 `request` 是个映射,结构如下: + +| Key | Type | Required | +| ------------------- | ---------------------------------- | -------- | +|`:body` |`java.io.InputStream` | | +|`:headers` |`{String String}` | Yes | +|`:protocol` |`String` | Yes | +|`:query-string` |`String` | | +|`:remote-addr` |`String` | Yes | +|`:request-method` |`Keyword` | Yes | +|`:scheme` |`Keyword` | Yes | +|`:server-name` |`String` | Yes | +|`:server-port` |`Integer` | Yes | +|`:ssl-client-cert` |`java.security.cert.X509Certificate`| | +|`:uri` |`String` | Yes | + +Handler 也应该返回一个映射,结构如下: + +| Key | Type | Required | +| -------- | ------------------------------------------ | -------- | +|`:body` |`ring.core.protocols/StreamableResponseBody`| | +|`:headers`|`{String String}` or `{String [String]}` | Yes | +|`:status` |`Integer` | Yes | + +上述表格摘自 [Ring Spec](https://github.com/ring-clojure/ring/blob/master/SPEC.md)。大部分情况下我们都只需要使用 `:body` 和 `:headers`,也就是响应头(或请求头)以及响应体(或请求体),响应时还需要戴上 `:status`,即 HTTP 状态码。 + +当然,如果你不用任何路由库,纯手动路由,那还要用到 `:request-method`(请求方法,如 `GET` `POST`)和 `:uri`(用来解析路径)。 + +Handler 做的事情其实就是对请求做些处理,然后构造响应。比方说,如果客户端用 `GET` 方法请求 `/hello` 目录才返回 `Hello!`,对其他路由都返回 `404` 和 `Not Found`,就可以这样写: + +```clojure +(ns antfighter.handler + (:import [java.net URI])) + +(defn handler [request] + (let [method (:request-method request) + uri (:uri request) + path (.getPath (URI. uri))] + (if (and (= method :get) + (= path "/hello")) + {:status 200 + :body "Hello!"} + {:status 404 + :body "Not Found"}))) +``` + +不过,访问 `/hello` 会得到 `Hello!`,但访问 `/hello/` 却是 `Not Found`。我们的路由匹配方式有缺陷。处理这些细节太麻烦了,有没有别的办法? + +## 路由器 + +--- + +## 其他参考资料 + +- 除了 Ring 默认的 Jetty,我个人比较推荐 [http-kit](https://github.com/http-kit/http-kit),它提供了简单且性能较好的 Web 服务器实现,可以用 `org.httpkit.server/run-server` 替代 `run-jetty`。这个库也提供了 HTTP 客户端实现。 + +[^1]: Jetty 是个 Java Web 服务器;也可以换成别的,这里就用默认的 Jetty 演示了。 + +[^2]: 上篇文章提到了,如果要用 Java 运行,就要保留 `core.clj` 中的 `:gen-class`。 |
