關於 web service, unity, blogger 等軟體工程筆記

HTTP router in golang

Edit icon 沒有留言
HTTP URL Router, this image is original By Renée French (http://reneefrench.blogspot.com) [CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0)]

工作需求需要使用 golang 來建立 Restful API Server,在開始實作前,最優先想到的問題是:如何處理 URL Routing。從 golang 的官方文件中,要建立一個 HTTP 服務可以透過以下的方式:

但仔細去看內部的實作後,發現 net/http 的 router 不符合所需,比如說不支援 URL 變數等。最後在網路上找到許多有用的資料,以下是在實作該需求時,所使用的設計 (design) 以及套件 (package) 的介紹。

URL Routing

從網路所搜尋到的 router solution,最多人一致推薦是 gorilla/mux,不僅有提供 URL 變數,也有 Get/POST method 比對,甚至建立巢狀的 router (nested routes),更重要的一點是,它跟 net/http 兼容 (compatible)。

r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)
r.HandleFunc("/hi", HelloHandler)
r.HandleFunc("/articles/{name}", ArticleHandler)

http.ListenAndServe(":80", r)

使用 mux 在建立 URL 變數不難,只要使用 {name} 這樣的形式,並透過 mux.vars(*http.Request) 就可以取得該變數資料。甚至還可以使用正規表示式來定義 (regular expression) 該變數:

r := mux.NewRouter()
r.HandleFunc("/articles/{id:[0-9]+}", func(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
w.Write([]byte(fmt.Sprintf("Articles, id: %v", vars["id"])))
})

建立 subrouters,用來建立巢狀路由:

r := mux.NewRouter()

sr := r.PathPrefix("/articles").Subrouter()
sr.HandleFunc("/article1", Article1Handler) // http://127.0.0.1/articles/article1
sr.HandleFunc("/article2", Article2Handler) // http://127.0.0.1/articles/article2

http.ListenAndServe(":80", r)

限制 method:

r := mux.NewRouter()
r.HandleFunc("/", MyHandler).Methods("GET", "POST")

找不到的自訂處理:

r := mux.NewRouter()
r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Not found"))
w.WriteHeader(http.StatusNotFound)
})

一個較完整的範例:

Middleware

接著要處理權限驗證 authority,當用戶端送進來的 request,要先經過權限驗證程序後,才執行之後的回應處理。這可以透過以下的設計模式 (design pattern) 來實做,middleware:

func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
// Do before handle
next(w, req)
// Do after handle
}
}

func main() {
r := mux.NewRouter()
r.HandleFunc("/", AuthMiddleware(HomeHandler))

http.ListenAndServe(":80", r)
}

因此只要把 AuthMiddleware 修改一下,就可以成為驗證用戶端權限的 middleware:

func AuthHandler(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
if !CheckAuthority(req){
w.WriteHeader(http.StatusUnauthorized)
return
}

next(w, req)
}
}

基於這一種 pattern,codegangsta/negroni 提供不錯的設計,協助包裝 middleware,且已經存在許多開發社群,社群開發很多有用的 middlewares,像說 gzip 壓縮資料,或是處理 session 等等。此外 codegangsta/negroni 也是兼容於 net/http,因此可以容易跟 gorilla/mux 做結合。

r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)

n := negroni.New(Middleware1, Middleware2)
n.Use(Middleware3)
n.UseHandler(r)

http.ListenAndServe(":80", n)

像上面的例子,一個用戶端 request,會先經過 Middleware1、Middleware2 以及 Middleware3 順序處理後,才會送給 mux 進行路由處理。這些 middlewares 可以包含許多應用,下一章會提到開發中所使用到的 middlewares。

如何讓 negroni 跟 mux subrouter 做結合,讓 subrouter 先經過指定 middlewares?目前沒有一個很好的做法,但還是有些取巧的方式。以下範例看似可以結合,但執行上進入 /admin 硬是執行了兩次 route 判斷,不知道在大型的 routers 的效能如何:

r := mux.NewRouter()

s := mux.NewRouter()
sr := s.PathPrefix("/admin").Subrouter() // PathPrefix is must same as HandlePath at line 8
sr.HandleFunc("/users", UsersListHandler)
sr.HandleFunc("/info", InfoHandler)

r.Handle("/admin", negroni.New(
AdminMiddleware,
negroni.Wrap(s),
))

http.ListenAndServe(":80", r)

或是把上述例子改成每一個 handler 都設定所需處理的 middlewares:

r := mux.NewRouter()
r.Handle("/admin/users", negroni.New(
AdminMiddleware,
negroni.Wrap(UsersListHandler),
))
r.Handle("/admin/info", negroni.New(
AdminMiddleware,
negroni.Wrap(UsersListHandler),
))

http.ListenAndServe(":80", r)

Middlewares

Recovery

發生 panic 時的攔截處理,通常是希望記錄 Log 以及回傳 HTTP 500 的錯誤,negroni 已經有提供一個不錯的 recovery middleware:

negroni.NewRecovery()

在開發階段中,這部分是自訂自己的 negroni-middleware:

func Recover(stackSize int, stackAll bool) negroni.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
defer func() {
if err := recover(); err != nil {
stack := make([]byte, stackSize)
stack = stack[:runtime.Stack(stack, stackAll)]

w.Header().Set("X-ERROR", fmt.Sprintf("%v, %v", err, string(stack)))
w.WriteHeader(http.StatusInternalServerError)
}
}()

next(w, req)
}
}

Gzip

回傳資料 (response) 的壓縮處理,減少傳輸數據,negroni-gzip

package main

import (
"net/http"

"github.com/codegangsta/negroni"
"github.com/phyber/negroni-gzip/gzip"
)

func main() {
r := http.NewServeMux()

n := negroni.New()
n.Use(gzip.Gzip(gzip.DefaultCompression))
n.UseHandler(r)

http.ListenAndServe(":80", n)
}

CORS

跨來源資源共享 (Cross-Origin Resource Sharing) 規範實作,要規避瀏覽器安全性存取限制,在開發階段中都不限制跨網域存取,使用 rs/cors
package main

import (
"net/http"

"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
"github.com/rs/cors"
)

func main() {
r := mux.NewRouter()

n := negroni.New()
n.Use(cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowCredentials: true,
}))
n.UseHandler(r)

http.ListenAndServe(":80", n)
}

Context

在自訂的 middleware 中,有時希望將處理完的參數,繼續傳遞給下一個 middlewares 或是 handlers,但在 negroni 的機制中並沒有辦法傳遞額外參數。因此要使用別的套件,這邊使用 gorilla/context 來完成參數傳遞:

package main

import (
"fmt"
"net/http"

"github.com/codegangsta/negroni"
"github.com/gorilla/context"
"github.com/gorilla/mux"
)

func HomeHandler(w http.ResponseWriter, req *http.Request) {
value := context.Get(req, "Key")
fmt.Fprintf(w, "Key is %v", value)
}

func MiddlewareHandler(keyValue string) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
context.Set(req, "Key", keyValue)
}
}

func main() {
r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)

n := negroni.New()
n.UseHandler(MiddlewareHandler("1234"))
n.UseHandler(r)

http.ListenAndServe(":80", context.ClearHandler(n))
}

context 內部使用 map[interface{}]interface{} 實作,所以 Key/Value 可以是任意的資料,特別要注意的是 Line 31 的 context.ClearHandler,在一個 request 處理完後,就立刻清除釋放該次 request 所配置的 context 記憶體資源。若不如此做的話,應要有機制定時呼叫 context.Purge() 來釋放記憶體資源。

Graceful

在關閉伺服器服務時,能夠給予一段時間緩衝,待 request 在某段時間內完成回應後,再關閉 HTTP 連線。雖不是很清楚對於需求有沒有必要,但還是加上去。使用 tylerb/graceful 方式也很簡單:

package main

import (
"time"

"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
"gopkg.in/tylerb/graceful.v1"
)

func main() {
r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)

n := negroni.New()
n.UseHandler(r)

graceful.Run(":80", 30*time.Second, n)
}

Renderer

因為資料是採用 JSON 格式,採用 unrolled/render 這一個套件來處理 JSON HTTP Heades 以及內容輸出,此外這套件也可以輸出 XML、binary 以及 HTML 等格式:

import (
"net/http"
"github.com/unrolled/render"
)

var Renderer *render.Render = render.New()

func HomeHandler(w http.ResponseWriter, r *http.Request) {
v := struct {
Message string
}{
"Hello World!",
}

Renderer.JSON(w, http.StatusOK, v)
}

Quick Start Example

綜合以上,以下提供一個可以快速開始的 HTTP route 範例,得先安裝所有必要的 package。另外更多套件的說明,請參考各個套件的 github 資訊。

go get github.com/gorilla/mux
go get github.com/codegangsta/negroni
go get github.com/phyber/negroni-gzip
go get github.com/rs/cors
go get github.com/gorilla/context
go get gopkg.in/tylerb/graceful.v1
go get github.com/unrolled/render

沒有留言: