Web 服务不只是「写个 Ring handler」。现代业务要调用下游 HTTP API、要支撑实时双向通信、要对接跨语言服务——这些都属于网络服务层的工程问题。本文以 Clojure 生态为背景,系统讲解 HTTP 客户端选型、WebSocket 双向通信、SSE 服务端推送、gRPC 跨语言集成,以及超时、重试、连接池和背压等网络健壮性工程。
1. HTTP 客户端选型
1.1 主流库对比
| 库 | 同步/异步 | 特点 | 适用 |
|---|---|---|---|
| clj-http | 同步(阻塞) | 类 Apache HttpClient,功能全 | 传统阻塞调用 |
| http-kit | 异步(非阻塞) | 轻量、基于 Netty | 高并发、服务端+客户端 |
| clj-http.fake | 同步 | 测试替身 | 单元测试 mock |
| aleph | 异步(流式) | 基于 Netty,流式响应 | 流式大响应 |
判断:同步调用代码简单,但一个阻塞调用会占一个线程;高并发场景(如爬虫、BFF 聚合)优先
http-kit异步,配合 /clojure-concurrency-patterns/ 的 core.async 编排并发。
2. clj-http 同步客户端实战
2.1 基本调用与请求定制
(require '[clj-http.client :as http])
;; 带超时、连接池、重定向控制的请求
(def resp
(http/get "https://api.example.com/orders"
{:query-params {:page 1 :size 20}
:headers {"Authorization" "Bearer xxx"
"Accept" "application/json"}
:timeout 3000
:conn-timeout 1000
:socket-timeout 3000
:as :json
:accept :json}))
(:status resp) ; => 200
(:body resp) ; => 解析后的 JSON 数据
2.2 重试与退避
网络调用不可避免会失败,工程化要包一层「指数退避重试」:
(defn request-with-retry [f & {:keys [retries delay] :or {retries 3 delay 100}}]
(loop [n retries]
(let [res (try (f) (catch Exception e (assoc {} :error e)))]
(cond
(and (contains? res :error) (> n 0))
(do (Thread/sleep delay) (recur (dec n)))
(and (contains? res :status)
(>= (:status res) 500) ; 5xx 可重试
(> n 0))
(do (Thread/sleep delay) (recur (dec n)))
:else res))))
(request-with-retry #(http/get "https://api.example.com/health"))
3. http-kit 异步客户端与服务端
3.1 异步 HTTP 调用
(require '[org.httpkit.client :as http])
;; 异步发起请求,返回 promise/future,不阻塞线程
(def future-resp
(http/get "https://api.example.com/orders"
{:timeout 3000
:query-params {:page 1}}))
;; 后续任意时刻取结果(可配合 channel)
(let [resp @future-resp] ; deref 阻塞直到完成
(:status resp))
3.2 用 core.async 编排并发聚合
(require '[clojure.core.async :as async])
;; BFF 场景:并行调用多个下游,聚合返回
(defn aggregate []
(let [ch (async/chan 3)
_ (async/go (async/>! ch (:body @(http/get url-a))))
_ (async/go (async/>! ch (:body @(http/get url-b))))
_ (async/go (async/>! ch (:body @(http/get url-c))))]
{:a (async/<!! ch) :b (async/<!! ch) :c (async/<!! ch)}))
3.3 http-kit 作为服务端
http-kit 既是客户端也是高性能服务端(Ring 兼容):
(require '[org.httpkit.server :as hk])
(defn handler [req] {:status 200 :body "hello"})
(hk/run-server handler {:port 8080 :threads 4 :queue-size 10000})
4. WebSocket 双向通信
4.1 服务端 WebSocket
实时应用(聊天、协同、行情推送)需要双向长连接。http-kit 原生支持 WebSocket:
(require '[org.httpkit.server :as hk])
;; 全局连接注册表
(def connections (atom {}))
(defn ws-handler [req]
(hk/with-channel req ch
(hk/on-receive ch (fn [msg]
(let [parsed (json/parse-string msg true)]
(case (:op parsed)
:join (swap! connections assoc (:uid parsed) ch)
:say (doseq [[_ c] @connections]
(hk/send! c (:text parsed)))))))
(hk/on-close ch (fn [_] (doseq [[uid c] @connections] (when (= c ch) (swap! connections dissoc uid)))))))
;; 路由:WebSocket 路径
(def routes {"/ws" ws-handler})
要点:WebSocket 长连接与 HTTP 不同——连接是「资源」,必须管理注册表、心跳保活与断开清理,否则会泄漏连接。
4.2 客户端 WebSocket
(require '[clj-websocket.client :as ws])
(def conn @(ws/connect "wss://example.com/ws"))
(ws/send-msg conn (json/write-str {:op :say :text "hello"}))
5. SSE:服务端单向推送
SSE(Server-Sent Events)比 WebSocket 更轻——只需 HTTP 长连接,服务端单向推送事件,浏览器原生支持 EventSource。适合行情、进度、通知:
;; Ring handler 返回 text/event-stream,持续推送
(defn sse-handler [req]
(hk/with-channel req ch
(async/go-loop []
(let [event (async/<! (event-chan))]
(hk/send! ch (str "data: " (json/write-str event) "\n\n")))
(recur))
{:status 200
:headers {"Content-Type" "text/event-stream"
"Cache-Control" "no-cache"}}))
| 对比 | WebSocket | SSE |
|---|---|---|
| 方向 | 双向 | 服务端 → 客户端 |
| 协议 | 独立协议 | HTTP 长连接 |
| 浏览器支持 | 需库 | EventSource 原生 |
| 自动重连 | 需自建 | 原生支持 |
| 适用 | 聊天/协作 | 推送/行情/进度 |
6. gRPC 跨语言调用
6.1 为什么用 gRPC
服务间调用(微服务、跨语言)用 gRPC 优于 HTTP+JSON:强类型 IDL、二进制 Protobuf 序列化、HTTP/2 多路复用、自带流式与错误码。
6.2 Clojure 集成
Clojure 用 Java 生态的 gRPC 库(io.grpc),配合 lein-protoc 或 protoc-jar 生成 stub:
;; 定义 proto → 生成 OrderServiceGrpc
(require '[OrderServiceGrpc :as grpc-stub])
(require '[io.grpc.ManagedChannelBuilder])
(def channel
(.build (ManagedChannelBuilder/forAddress "order-svc" 9090)
(.usePlaintext)))
;; 一元调用
(def stub (grpc-stub/newBlockingStub channel))
(def resp (.placeOrder stub
(Order$PlaceOrderRequest/newBuilder
(.setUserId ...) (.setAmount ...) (.build))))
;; 流式调用:请求/响应的流式接口同样可用
判断:单体内部不要上 gRPC(复杂化);跨团队、跨语言、高吞吐的内部服务间通信才值得。对外 API 仍以 REST/OpenAPI 为主。
7. 网络健壮性工程
7.1 超时与连接池
| 维度 | 建议 |
|---|---|
| 连接超时 | 1-2s,快速失败 |
| 读超时 | 业务决定,通常 3-10s |
| 连接池上限 | 按 QPS 与下游 RT 估算 |
| 空闲回收 | 启用 keep-alive 与空闲清理 |
7.2 背压与限流
下游变慢时,上游要「减速」而不是堆积:
;; 用 core.async 通道缓冲 + 背压
(def chan (async/chan 100)) ; 缓冲 100,满则 send! 阻塞/丢弃
;; 配合信号量限流,防止打垮下游
(def sem (java.util.concurrent.Semaphore. 50))
(defn throttled-call [f]
(.acquire sem)
(try (f) (finally (.release sem))))
8. 常见陷阱
| 陷阱 | 现象 | 规避 |
|---|---|---|
| 忘设超时 | 调用挂死 | 所有请求显式 timeout |
| 连接泄漏 | 句柄耗尽 | 用后 close,或用连接池 |
| 重试风暴 | 下游被压垮 | 加退避与 jitter,限制重试次数 |
| 阻塞混入异步 | 事件循环卡死 | 异步 handler 内勿做阻塞 IO |
| 背压缺失 | 内存堆积 | 通道设缓冲上限 |
9. 总结
Clojure 网络服务层的能力图谱:HTTP 同步调用用 clj-http(带超时与重试封装)、高并发异步用 http-kit(配合 core.async 编排并发聚合)、实时双向用 WebSocket、单向推送用 SSE、跨语言内部通信用 gRPC。网络工程的核心不是「会用某个库」,而是把超时、重试、连接池、背压当成一等公民设计。结合 /clojure-microservices-architecture/ 的架构实践与 /clojure-data-pipeline/ 的流处理,这套网络能力可以支撑起从 BFF 聚合到实时推送的完整服务形态。
延伸阅读
- Clojure 并发设计模式 — core.async 编排网络并发
- Clojure 现代 Web 全栈开发 — Ring/reitit 服务端骨架
- Clojure 微服务架构实战 — 服务间通信的整体设计
- 网络专题 — HTTP、WebSocket 与负载均衡原理
- Java 专题 — Java 网络库与 NIO 对照
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。