Clojure: Ring 中間件原理剖析

Ring 提供了 web 開發所需的基礎構件,好比處理請求參數,cookie, session 等等。經過向 http-kit 或 jetty 註冊 handler 的方式來提供服務。handler 函數接收一個 request 參數,此參數由調用者傳遞(此處爲 http-kit 或 jetty)。然而 http-kit 或 jetty 傳遞過來的 request 參數只包含了基本標準鍵,Ring 將經過中間件的方式提供更高級的功能。web

前奏

如下爲 request 包含的基本標準鍵:服務器

:server-port ---------- 用於處理該請求的服務端口
:server-name ---------- 服務器的 IP 地址或是主機名
:remote-addr ---------- 客戶端的 IP 地址
:query-string --------- 請求的查詢字符串
:scheme --------------- 協議的類型,能夠是 HTTP 或者 HTTPS
:request-method ------- 請求的方法,好比::get、:head、:options、:put、:post 或 :delete
:request-string ------- 請求的查詢字符串
:content-type --------- 請求消息體的 MIME 類型
:content-length ------- 請求消息體的字節數
:character-encoding --- 請求採用的字符編碼名稱
:headers -------------- 包含了請求頭部的map
:body ----------------- 可用於讀取請求消息體的輸入流
:context -------------- 當應用沒有做爲根來部署時,其所處的上下文
:uri ------------------ 服務端的 URI 全路徑,包含了 :context(若是存在)的部分
:ssl-client-cert ------ 客戶端的 SSL 證書

* 注意:此處列出的鍵,並不必定會出如今全部的請求中,好比 :ssl-client-cert

 

Ring 中間件的原理其實很簡單。鏈式執行,前一個的輸出做爲下一個的輸入,有點相似於管道。在深刻講解 Ring 的中間件前,咱們先來熟悉 Clojure 提供的一個方法:->。cookie

方法 -> 就支持鏈式執行,它接受任意多個參數。第一個參數能夠爲任意形式,剩下的參數必須爲函數,而且至少接受一個參數。這是由於方法 -> 實現的功能是:將前一個表達式的結果做爲下一個函數的第一個參數來調用。執行順序是先上後下,舉例以下:session

(defn test-a [msg]
  (let [_msg (str msg " -- a")]
    (println _msg)
    _msg))

(defn test-b [msg]
  (let [_msg (str msg " -- b")]
    (println _msg)
    _msg))

(defn test-c [msg]
  (let [_msg (str msg " -- c")]
    (println _msg)
    _msg))

(defn -main [& args]
  (->
    (test-a "<testing>")
    test-b
    test-c))

 

在命令行執行:閉包

$ lein run
<testing> -- a
<testing> -- a -- b
<testing> -- a -- b -- c

 

能夠看到,前一個表達式的值做爲下一個函數的第一個參數。這種內置的語法令鏈式執行看起來一目瞭然,不需像其它語言那樣層層閉包。固然若是 Clojure 沒有內置這樣的函數,也能夠經過定義宏的方式來實現。app

Ring 中間件機制

Ring 中間件其實就是經過 -> 來進行鏈式執行。還記得須要向 http-kit 或 jetty 註冊一個 handler 嗎?Ring 中間件的原理說白了就是對 handler 進行層層包裝,返回一個新的 handler。可是由於返回了一個新的 handler,也是就是閉包,對於真正的執行時機可能會有點不太同樣。接下來舉例說明:函數

(defn my-handler [req]
  (println "my-handler")
  "my-handler response")

(defn test-a [handler]
  (fn [req]
    (println "test-a")
    (handler req)))

(defn test-b [handler]
  (fn [req]
    (println "test-b")
    (handler req)))

(defn test-c [handler]
  (fn [req]
    (println "test-c")
    (handler req)))

(def app
  (->
    my-handler
    test-a
    test-b
    test-c))

(defn -main [& args]
  (app nil))

 

在命令行執行:post

$ lein run
test-c
test-b
test-a
my-handler

 

結果看起來跟以前說的不太同樣。以前說的執行順序是先上後下,此次的結果明顯是先下後上,到底是哪裏出現問題了呢?其實兩次的執行結果都是正確的。前一次是簡單的鏈式執行,而這一次鏈式執行發生後,每一步都返回了一個閉包,致使了延遲執行。相似如下代碼:編碼

(->
    my-handler
    test-a
    test-b
    test-c)

 

其實能夠當作:spa

(test-c (test-b (test-a my-handler)))

 

展開後:

(println "test-c")
; test-b 返回的閉包
(println "test-b")
; test-a 返回的閉包
(println "test-a")
; my-handler
(println "my-handler")

 

至此想必你已經對 Ring 的中間件有所瞭解了。

中件間能幹什麼

Ring 提供了一些基礎的中間件,它們或多或少的對 request 和 response 進行修改,以達到特定目的。ring-devel 對開發環境提供了支持,好比 wrap-reload 容許你沒必要重啓便可在修改源碼後自動從新載入等等。ring-core 提供了更多標準中間件,有wrap-params、wrap-not-modified、wrap-content-type等等。

Ring 中間件的執行流程相當重要,將來不少時候都要跟中間件打交道,好比自定義攔截,實現公共處理(其它語言中多是經過繼承父控制器來實現)等等。

結束語

Ring 是一個很優秀的針對 web 開發的基礎構件。研究它的原理和熟讀它的源碼不論是在 web 開發上仍是對於 Clojure 的理解上都有莫大的好處。

相關文章
相關標籤/搜索