Clojure 网络服务深入:HTTP 客户端、WebSocket、SSE 与 gRPC

深入 Clojure 的网络服务层:http-kit 异步 HTTP 客户端与服务端、clj-http 同步调用、WebSocket 双向通信、SSE 服务器推送、gRPC 跨语言调用,以及网络超时、重试、连接池与背压的工程化处理,帮你在 Web 服务中稳健地调用与提供网络能力。

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"}}))
对比WebSocketSSE
方向双向服务端 → 客户端
协议独立协议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」更多文章

  1. Clojure 函数式错误处理:Result、异常与结构化错误
  2. Clojure GraphQL API 实战:lacinia、Schema、Resolver 与权限
  3. Clojure REPL 驱动开发:nREPL、热重载与交互式工作流