Linux中國

使用 gorilla/mux 進行 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 main

import (
   "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 = 1
var crudRequests chan *crudRequest

// GET /
// GET /cliches
func ClichesAll(res http.ResponseWriter, req *http.Request) {
   cr := &crudRequest{verb: GETALL, confirm: make(chan string)}
   completeRequest(cr, res, "read all")
}

// GET /cliches/id
func 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 /cliches
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")
}

// PUT /cliches/id
func 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/id
func 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 list
func addPair(cp *clichePair) string {
   cp.Id = masterId
   masterId++
   clichesList = append(clichesList, cp)
   return "nCreated: " + cp.Cliche + " " + cp.Counter + "n"
}

// Edit an existing clichePair
func 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 clichePair
func 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 + " deletedn"
   }
   return msg
}

//*** utility functions
func 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 應用程序突出了軟體包的主要功能。

via: https://opensource.com/article/18/8/http-request-routing-validation-gorillamux

作者:Marty Kalin 選題:lujun9972 譯者:yongshouzhang 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國