最近吃飽閒閒就寫了一個Go的伺服器框架
當然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用到的所有技術
就是專案原始碼所在的位置
沒有留言:
張貼留言