使用 gorilla/mux 進行 HTTP 請求路由和驗證

2018-12-15 09:10:00

gorilla/mux 包以直觀的 API 提供了 HTTP 請求路由、驗證和其它服務。

Go 網路庫包括 http.ServeMux 結構型別,它支援 HTTP 請求多路複用(路由):Web 伺服器將託管資源的 HTTP 請求與諸如 /sales4today 之類的 URI 路由到程式碼處理程式;處理程式在傳送 HTTP 響應(通常是 HTML 頁面)之前執行適當的邏輯。 這是該體系的草圖:

             +-----------+     +--------+     +---------+HTTP 請求---->| web 伺服器 |---->| 路由   |---->| 處理程式  |             +-----------+     +--------+     +---------+

呼叫 ListenAndServe 方法後啟動 HTTP 伺服器:

http.ListenAndServe(":8888", nil) // args: port & router

第二個引數 nil 意味著 DefaultServeMux 用於請求路由。

gorilla/mux 庫包含 mux.Router 型別,可替代 DefaultServeMux 或自定義請求多路複用器。 在 ListenAndServe 呼叫中,mux.Router 範例將代替 nil 作為第二個引數。 下面的範例程式碼很好的說明了為什麼 mux.Router如此吸引人:

1、一個簡單的 CRUD web 應用程式

crud web 應用程式(見下文)支援四種 CRUD(建立/讀取/更新/刪除)操作,它們分別對應四種 HTTP 請求方法:POST、GET、PUT 和 DELETE。 在這個 CRUD 應用程式中,所管理的資源是套話與反套話的列表,每個都是套話及其反面的的套話,例如這對:

Out of sight, out of mind. Absence makes the heart grow fonder.

可以新增新的套話對,可以編輯或刪除現有的套話對。

CRUD web 應用程式:

package mainimport (   "gorilla/mux"   "net/http"   "fmt"   "strconv")const GETALL string = "GETALL"const GETONE string = "GETONE"const POST string   = "POST"const PUT string    = "PUT"const DELETE string = "DELETE"type clichePair struct {   Id      int   Cliche  string   Counter string}// Message sent to goroutine that accesses the requested resource.type crudRequest struct {   verb     string   cp       *clichePair   id       int   cliche   string   counter  string   confirm  chan string}var clichesList = []*clichePair{}var masterId = 1var crudRequests chan *crudRequest// GET /// GET /clichesfunc ClichesAll(res http.ResponseWriter, req *http.Request) {   cr := &crudRequest{verb: GETALL, confirm: make(chan string)}   completeRequest(cr, res, "read all")}// GET /cliches/idfunc ClichesOne(res http.ResponseWriter, req *http.Request) {   id := getIdFromRequest(req)   cr := &crudRequest{verb: GETONE, id: id, confirm: make(chan string)}   completeRequest(cr, res, "read one")}// POST /clichesfunc ClichesCreate(res http.ResponseWriter, req *http.Request) {   cliche, counter := getDataFromRequest(req)   cp := new(clichePair)   cp.Cliche = cliche   cp.Counter = counter   cr := &crudRequest{verb: POST, cp: cp, confirm: make(chan string)}   completeRequest(cr, res, "create")}// PUT /cliches/idfunc ClichesEdit(res http.ResponseWriter, req *http.Request) {   id := getIdFromRequest(req)   cliche, counter := getDataFromRequest(req)   cr := &crudRequest{verb: PUT, id: id, cliche: cliche, counter: counter, confirm: make(chan string)}   completeRequest(cr, res, "edit")}// DELETE /cliches/idfunc ClichesDelete(res http.ResponseWriter, req *http.Request) {   id := getIdFromRequest(req)   cr := &crudRequest{verb: DELETE, id: id, confirm: make(chan string)}   completeRequest(cr, res, "delete")}func completeRequest(cr *crudRequest, res http.ResponseWriter, logMsg string) {   crudRequests<-cr   msg := <-cr.confirm   res.Write([]byte(msg))   logIt(logMsg)}func main() {   populateClichesList()   // From now on, this gorountine alone accesses the clichesList.   crudRequests = make(chan *crudRequest, 8)   go func() { // resource manager      for {         select {         case req := <-crudRequests:         if req.verb == GETALL {            req.confirm<-readAll()         } else if req.verb == GETONE {            req.confirm<-readOne(req.id)         } else if req.verb == POST {            req.confirm<-addPair(req.cp)         } else if req.verb == PUT {            req.confirm<-editPair(req.id, req.cliche, req.counter)         } else if req.verb == DELETE {            req.confirm<-deletePair(req.id)         }      }   }()   startServer()}func startServer() {   router := mux.NewRouter()   // Dispatch map for CRUD operations.   router.HandleFunc("/", ClichesAll).Methods("GET")   router.HandleFunc("/cliches", ClichesAll).Methods("GET")   router.HandleFunc("/cliches/{id:[0-9]+}", ClichesOne).Methods("GET")   router.HandleFunc("/cliches", ClichesCreate).Methods("POST")   router.HandleFunc("/cliches/{id:[0-9]+}", ClichesEdit).Methods("PUT")   router.HandleFunc("/cliches/{id:[0-9]+}", ClichesDelete).Methods("DELETE")   http.Handle("/", router) // enable the router   // Start the server.   port := ":8888"   fmt.Println("\nListening on port " + port)   http.ListenAndServe(port, router); // mux.Router now in play}// Return entire list to requester.func readAll() string {   msg := "\n"   for _, cliche := range clichesList {      next := strconv.Itoa(cliche.Id) + ": " + cliche.Cliche + "  " + cliche.Counter + "\n"      msg += next   }   return msg}// Return specified clichePair to requester.func readOne(id int) string {   msg := "\n" + "Bad Id: " + strconv.Itoa(id) + "\n"   index := findCliche(id)   if index >= 0 {      cliche := clichesList[index]      msg = "\n" + strconv.Itoa(id) + ": " + cliche.Cliche + "  " + cliche.Counter + "\n"   }   return msg}// Create a new clichePair and add to listfunc addPair(cp *clichePair) string {   cp.Id = masterId   masterId++   clichesList = append(clichesList, cp)   return "\nCreated: " + cp.Cliche + " " + cp.Counter + "\n"}// Edit an existing clichePairfunc editPair(id int, cliche string, counter string) string {   msg := "\n" + "Bad Id: " + strconv.Itoa(id) + "\n"   index := findCliche(id)   if index >= 0 {      clichesList[index].Cliche = cliche      clichesList[index].Counter = counter      msg = "\nCliche edited: " + cliche + " " + counter + "\n"   }   return msg}// Delete a clichePairfunc deletePair(id int) string {   idStr := strconv.Itoa(id)   msg := "\n" + "Bad Id: " + idStr + "\n"   index := findCliche(id)   if index >= 0 {      clichesList = append(clichesList[:index], clichesList[index + 1:]...)      msg = "\nCliche " + idStr + " deleted\n"   }   return msg}//*** utility functionsfunc findCliche(id int) int {   for i := 0; i < len(clichesList); i++ {      if id == clichesList[i].Id {         return i;      }   }   return -1 // not found}func getIdFromRequest(req *http.Request) int {   vars := mux.Vars(req)   id, _ := strconv.Atoi(vars["id"])   return id}func getDataFromRequest(req *http.Request) (string, string) {   // Extract the user-provided data for the new clichePair   req.ParseForm()   form := req.Form   cliche := form["cliche"][0]    // 1st and only member of a list   counter := form["counter"][0]  // ditto   return cliche, counter}func logIt(msg string) {   fmt.Println(msg)}func populateClichesList() {   var cliches = []string {      "Out of sight, out of mind.",      "A penny saved is a penny earned.",      "He who hesitates is lost.",   }   var counterCliches = []string {      "Absence makes the heart grow fonder.",      "Penny-wise and dollar-foolish.",      "Look before you leap.",   }   for i := 0; i < len(cliches); i++ {      cp := new(clichePair)      cp.Id = masterId      masterId++      cp.Cliche = cliches[i]      cp.Counter = counterCliches[i]      clichesList = append(clichesList, cp)   }}

為了專注於請求路由和驗證,CRUD 應用程式不使用 HTML 頁面作為請求響應。 相反,請求會產生明文響應訊息:套話對的列表是對 GET 請求的響應,確認新的套話對已新增到列表中是對 POST 請求的響應,依此類推。 這種簡化使得使用命令列實用程式(如 curl)可以輕鬆地測試應用程式,尤其是 gorilla/mux 元件。

gorilla/mux 包可以從 GitHub 安裝。 CRUD app 無限期執行;因此,應使用 Control-C 或同等命令終止。 CRUD 應用程式的程式碼,以及讀我檔案和簡單的 curl 測試,可以在我的網站上找到。

2、請求路由

mux.Router 擴充套件了 REST 風格的路由,它賦給 HTTP 方法(例如,GET)和 URL 末尾的 URI 或路徑(例如 /cliches)相同的權重。 URI 用作 HTTP 動詞(方法)的名詞。 例如,在HTTP請求中有一個起始行,例如:

GET /cliches

意味著得到所有的套話對,而一個起始線,如:

POST /cliches

意味著從 HTTP 正文中的資料建立一個套話對。

在 CRUD web 應用程式中,有五個函數充當 HTTP 請求的五種變體的請求處理程式:

ClichesAll(...)    # GET: 獲取所有的套話對ClichesOne(...)    # GET: 獲取指定的套話對ClichesCreate(...) # POST: 建立新的套話對ClichesEdit(...)   # PUT: 編輯現有的套話對ClichesDelete(...) # DELETE: 刪除指定的套話對

每個函數都有兩個引數:一個 http.ResponseWriter 用於向請求者傳送一個響應,一個指向 http.Request 的指標,該指標封裝了底層 HTTP 請求的資訊。 使用 gorilla/mux 包可以輕鬆地將這些請求處理程式註冊到Web伺服器,並執行基於正規表示式的驗證。

CRUD 應用程式中的 startServer 函數註冊請求處理程式。 考慮這對註冊,router 作為 mux.Router 範例:

router.HandleFunc("/", ClichesAll).Methods("GET")router.HandleFunc("/cliches", ClichesAll).Methods("GET")

這些語句意味著對單斜線 //cliches 的 GET 請求應該路由到 ClichesAll 函數,然後處理請求。 例如,curl 請求(使用 作為命令列提示符):

% curl --request GET localhost:8888/

會產生如下結果:

1: Out of sight, out of mind.  Absence makes the heart grow fonder.2: A penny saved is a penny earned.  Penny-wise and dollar-foolish.3: He who hesitates is lost.  Look before you leap.

這三個套話對是 CRUD 應用程式中的初始資料。

在這句註冊語句中:

router.HandleFunc("/cliches", ClichesAll).Methods("GET")router.HandleFunc("/cliches", ClichesCreate).Methods("POST")

URI 是相同的(/cliches),但動詞不同:第一種情況下為 GET 請求,第二種情況下為 POST 請求。 此註冊舉例說明了 REST 樣式的路由,因為僅動詞的不同就足以將請求分派給兩個不同的處理程式。

註冊中允許多個 HTTP 方法,儘管這會影響 REST 風格路由的精髓:

router.HandleFunc("/cliches", DoItAll).Methods("POST", "GET")

除了動詞和 URI 之外,還可以在功能上路由 HTTP 請求。 例如,註冊

router.HandleFunc("/cliches", ClichesCreate).Schemes("https").Methods("POST")

要求對 POST 請求進行 HTTPS 存取以建立新的套話對。以類似的方式,註冊可能需要具有指定的 HTTP 頭元素(例如,認證憑證)的請求。

3、 Request validation

gorilla/mux 包採用簡單,直觀的方法通過正規表示式進行請求驗證。 考慮此請求處理程式以獲取一個操作:

router.HandleFunc("/cliches/{id:[0-9]+}", ClichesOne).Methods("GET")

此註冊排除了 HTTP 請求,例如:

% curl --request GET localhost:8888/cliches/foo

因為 foo 不是十進位制數位。該請求導致熟悉的 404(未找到)狀態碼。 在此處理程式註冊中包含正規表示式模式可確保僅在請求 URI 以十進位制整數值結束時才呼叫 ClichesOne 函數來處理請求:

% curl --request GET localhost:8888/cliches/3  # ok

另一個例子,請求如下:

% curl --request PUT --data "..." localhost:8888/cliches

此請求導致狀態程式碼為 405(錯誤方法),因為 /cliches URI 在 CRUD 應用程式中僅在 GET 和 POST 請求中註冊。 像 GET 請求一樣,PUT 請求必須在 URI 的末尾包含一個數位 id:

router.HandleFunc("/cliches/{id:[0-9]+}", ClichesEdit).Methods("PUT")

4、並行問題

gorilla/mux 路由器作為單獨的 Go 協程執行對已註冊的請求處理程式的每次呼叫,這意味著並行性被內建於包中。 例如,如果有十個同時發出的請求,例如

% curl --request POST --data "..." localhost:8888/cliches

然後 mux.Router 啟動十個 Go 協程來執行 ClichesCreate 處理程式。

GET all、GET one、POST、PUT 和 DELETE 中的五個請求操作中,最後三個改變了所請求的資源,即包含套話對的共用 clichesList。 因此,CRUD app 需要通過協調對 clichesList 的存取來保證安全的並行性。 在不同但等效的術語中,CRUD app 必須防止 clichesList 上的競爭條件。 在生產環境中,可以使用資料庫系統來儲存諸如 clichesList 之類的資源,然後可以通過資料庫事務來管理安全並行。

CRUD 應用程式採用推薦的Go方法來實現安全並行:

  • 只有一個 Go 協程,資源管理器在 CRUD app startServer 函數中啟動,一旦 Web 伺服器開始偵聽請求,就可以存取 clichesList
  • 諸如 ClichesCreateClichesAll 之類的請求處理程式向 Go 通道傳送(指向)crudRequest 範例(預設情況下是執行緒安全的),並且資源管理器單獨從該通道讀取。 然後,資源管理器對 clichesList 執行請求的操作。

安全並行體系結構繪製如下:

            crudRequest                讀/寫請求處理程式 -------------> 資源託管者 ------------> 套話列表

在這種架構中,不需要顯式鎖定 clichesList,因為一旦 CRUD 請求開始進入,只有一個 Go 協程(資源管理器)存取 clichesList

為了使 CRUD 應用程式盡可能保持並行,在一方請求處理程式與另一方的單一資源管理器之間進行有效的分工至關重要。 在這裡,為了審查,是 ClichesCreate 請求處理程式:

func ClichesCreate(res http.ResponseWriter, req *http.Request) {   cliche, counter := getDataFromRequest(req)   cp := new(clichePair)   cp.Cliche = cliche   cp.Counter = counter   cr := &crudRequest{verb: POST, cp: cp, confirm: make(chan string)}   completeRequest(cr, res, "create")}

ClichesCreate 呼叫實用函數 getDataFromRequest,它從 POST 請求中提取新的套話和反套話。 然後 ClichesCreate 函數建立一個新的 ClichePair,設定兩個欄位,並建立一個 crudRequest 傳送給單個資源管理器。 此請求包括一個確認通道,資源管理器使用該通道將資訊返回給請求處理程式。 所有設定工作都可以在不涉及資源管理器的情況下完成,因為尚未存取 clichesList

請求處理程式呼叫實用程式函數,該函數從 POST 請求中提取新的套話和反套話。 然後,該函數建立一個新的,設定兩個欄位,並建立一個 crudRequest 傳送到單個資源管理器。 此請求包括一個確認通道,資源管理器使用該通道將資訊返回給請求處理程式。 所有設定工作都可以在不涉及資源管理器的情況下完成,因為尚未存取它。

completeRequest 實用程式函數在 ClichesCreate 函數和其他請求處理程式的末尾呼叫:

completeRequest(cr, res, "create") // shown above

通過將 crudRequest 放入 crudRequests 頻道,使資源管理器發揮作用:

func completeRequest(cr *crudRequest, res http.ResponseWriter, logMsg string) {   crudRequests<-cr          // 向資源託管者傳送請求   msg := <-cr.confirm       // 等待確認   res.Write([]byte(msg))    // 向請求方傳送確認   logIt(logMsg)             // 列印到標準輸出}

對於 POST 請求,資源管理器呼叫實用程式函數 addPair,它會更改 clichesList 資源:

func addPair(cp *clichePair) string {   cp.Id = masterId  // 分配一個唯一的 ID    masterId++        // 更新 ID 計數器   clichesList = append(clichesList, cp) // 更新列表   return "\nCreated: " + cp.Cliche + " " + cp.Counter + "\n"}

資源管理器為其他 CRUD 操作呼叫類似的實用程式函數。 值得重複的是,一旦 Web 伺服器開始接受請求,資源管理器就是唯一可以讀取或寫入 clichesList 的 goroutine。

對於任何型別的 Web 應用程式,gorilla/mux 包在簡單直觀的 API 中提供請求路由、請求驗證和相關服務。 CRUD web 應用程式突出了軟體包的主要功能。