2017年10月16日 星期一

實作http框架by Go ---- Rocket

最近吃飽閒閒就寫了一個Go的伺服器框架
靈感來自Rust的一個框架 rocket.rs

當然Go與Rust有許多的相異之處,例如Go完全沒有泛型,也沒有巨集等等
因此兩個框架大概只有皮像吧
Rocket for Go大致上看起來像是這樣(!!! 這是舊API)
import (
         "fmt"
         "github.com/dannypsnl/rocket"
)

var hello = rocket.Handler {
        Route: "/name/:name/age/:age",
        Do:    func(Context map[string]string) string {
                return fmt.Sprintf("Hello, %s\nYour age is %s", Context["name"], Context["age"])
        },
}

func main() {
        rocket.Ignite(":8080").
            Mount("/hello", hello).
            Launch() // Start Serve
}
對比一下Rust的原版
#![feature(plugin, decl_macro)]
#![plugin(rocket_codegen)]

extern crate rocket;

#[get("/<name>/<age>")]
fn hello(name: String, age: u8) -> String {
    format!("Hello, {} year old named {}!", age, name)
}

fn main() {
    rocket::ignite().mount("/hello", routes![hello]).launch();
}
看起來差不多

那麼這一切是怎麼實現的呢?
最開始我直接做了一個簡單的對應,把rocket.Handler轉換成Go http程式庫的HandleFunc能夠接受的格式中處理
大概像這樣
func (r *Handler) toHandleFunc() func(http.ResponseWriter, *http.Request) {
        return func(w http.ResponseWriter, req *http.Request) {
                 fmt.Fprintf(w, "%v\n", r.Do())
        }
}

不過很快就發現原生的http程式庫並不支援動態路由(也就是/:name等可部份替換的路由)
最後敲定的解決辦法是用regexp套件進行路由的比較,以取得對應的Handler
因此我們要能夠在註冊程式取得路由的regex以及參數
func (r *Rocket) Mount(route string, h Handler) *Rocket {
 route += h.Route
 match, params := splitMountUrl(route)
 h.params = params
 r.matchs = append(r.matchs, match)
 r.handlers[match] = h
 return r
}
這就是Mount的完整實現,註冊路由由兩個部份組成,一個是Mount函式自帶的路由,另一部份是Handler擁有的路由
結合之後用splitMountUrl這個輔助函式切分出路由的regex模式以及參數
接下來將參數有哪些告訴handler
然後將regex模式放進全域中的regex模式陣列中
並且將regex模式與handler對應起來

那麼splitMountUrl究竟是如何實現的呢?
const legalCharsInUrl = "([a-zA-Z0-9-_]+)"

func splitMountUrl(route string) (string, []string) {
 var match string
 var params []string
 // '/:id' is params in url.
 // '/*filepath' is params about filepath.
 // '/home, data' is params from post method.
 for _, url := range strings.Split(route, "/") {
  if strings.HasPrefix(url, ":") {
   match += "/" + legalCharsInUrl
   params = append(params, url[1:])
  } else if strings.HasPrefix(url, "*") {
   match += "/.*?"
   params = append(params, url[1:])
   break
  } else if url != "" {
   match += "/" + url
  }
 }
 if match == "" {
  match = "/"
 }
 return match, params
}
這是追求簡單化的實作,非常沒有效率
但是暫時就這樣了
ps. 第三種對應我還未實作
首先用/字串切分路由,接著如果是:開頭的就是參數部份,我們就插入一個legalCharsInUrl字串
這個字串是我們能在路由中接受的字元集的regex表示法
那麼*則是路徑參數,那麼之後的路由都屬於路徑,因此不再處理
最後是!= ""是因為Split後的陣列會有空字串,這會導致錯誤,因此略過

接著我們看Launch的實作
func (rk *Rocket) Launch() {
 http.HandleFunc("/", rk.ServeHTTP)
 log.Fatal(http.ListenAndServe(rk.port, nil))
}
簡單到不行
我們提交唯一的HandlerFunction(原生http程式庫的handler)
然後啟動伺服器
最後來到匹配路由的核心部份
rocket.ServeHTTP
func (rk *Rocket) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 var match string
 for _, m := range rk.matchs { // rk.matchs are those static routes
  if m != "/" {
   matched, err := regexp.MatchString(m, r.URL.Path)
   if matched && err == nil {
    match = m
    break
   }
  } else {
   if m == r.URL.Path {
    match = m
   }
  }
 }
 fmt.Printf("Rquest URL: %#v\n", r.URL.Path)
 h := rk.handlers[match]
 matchEls := strings.Split(match, "/")
 Context := make(map[string]string)
 splitRqUrl := strings.Split(r.URL.Path, "/")
 j := 0
 for i, p := range splitRqUrl {
  if matchEls[i] == legalCharsInUrl {
   Context[h.params[j]] = p
   j++
  } else if matchEls[i] == ".*?" {
   Context[h.params[j]] = strings.Join(splitRqUrl[i:], "/")
   break
  }
 }

 fmt.Fprintf(w, h.Do(Context))
}
輪詢rocket中的matchs(還記得吧,我們在Mount中看過)
去除/是因為在正規表達式中它會匹配走所有路由,而那不是我們預期的行為
其餘路由如果成功(matched==true)匹配到request.URL.Path,就把match變數改成這個regex
因為接著我們要靠這個值取得對應的handler(rocket.handlers[match])

接著我們把match與r.URL.Path用Split切開,如果是match的子字串是 ':' 也就是(legalCharsInUrl)
那麼就把對應的splitRqUrl的值放入
例如:
match: /hello/name/:name/age/:age
r.URL.Path: /hello/name/danny/age/20
其Context就是
{
  "name": "danny",
  "age": "20"
}

至於 '*' 也就是 ".*?" 則是把接下來的整段路由都放入Context,這需要用Join恢復被切開的字串
最後將Context傳入Handler.Do並寫出其執行結果

這就是Rocket for Go用到的所有技術
最後這裡 => Rocket for Go
就是專案原始碼所在的位置

沒有留言:

張貼留言