summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-10-03 21:18:59 +0800
committereltrac <hi@eltr.ac>2026-10-03 21:18:59 +0800
commit7b98a8c41a5d16001e8e609f1e12dd336630bb7d (patch)
tree9ba9234654e4a3f32a708f690b2f98011ce58d11
parent69d55b7a2498f40daa5b13a2090352db5bd3f7e2 (diff)
[draft] new
-rw-r--r--content/article/clj6.md165
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 协议相关的细节。
+
+![](https://img.eltr.ac/geedeapro/article/clj6/ring-concepts.svg "图片由我使用 [Excalidraw](https://excalidraw.com/) 绘制")
+{.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`。