這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
不知道大家還記得不記得大約一年前,我的一個白日夢《關於Web編程非同步模型的白日夢》,然後這個白日夢我又連續做了好幾天《Web編程非同步模型的PHP原生實現》、《Web編程非同步模型的 Gearman 實現(殘)》。
當時怎麼也沒相通,還死皮白賴的粘在PHP的非同步實現上不肯放手。好吧,實現是繁瑣的,應用是成功的,代碼是容易寫的,環境是要搭建的……
昨晚睡覺前突然覺得自己應該真正用Go實現一下非同步Web,哪怕是個例子也好啊。於是,邊吃飯,邊敲了一票代碼搞了一個很簡單的demo,分享給大家吧。在這裡下載完整的代碼:webdemo。
包含以下檔案:
- async.go——非同步Web
- data.go——類比資料
- Makefile——不說了,你懂……
- page.go——頁面
- sync.go——同步Web
- timer.go——記錄執行時間的日誌
- webdemo.go——主檔案
編譯並運行
make run
通過瀏覽器分別訪問同步方式的http://127.0.0.1:8888/sync,和非同步方式的http://127.0.0.1:8888/async。在控制台會輸出請求處理的時間長度,實際上即便不看統計時間長度,兩者之間的速度差異很直觀的能體會出來。
mikespook@mikespook-desktop:~/Desktop/webdemo$ make run8g -o _go_.8 timer.go data.go page.go async.go sync.go rm -f _obj/webdemo.agopack grc _obj/webdemo.a _go_.8 cp _obj/webdemo.a "/home/mikespook/bin/go/pkg/linux_386/webdemo.a"8g webdemo.go8l -o webdemo webdemo.8./webdemo2011/03/25 15:06:35 async:2286230002011/03/25 15:06:57 sync:20024992000
核心思路很簡單,我在類比資料請求的代碼裡用了 time.Sleep(),讓每次資料請求都延遲 0.2 秒(一個最佳化得不太好的,很大的資料庫表的一次很爛的查詢所用時間):
// data.gofunc GetContents(key string) string { time.Sleep(SLEEP) // 延遲 return fmt.Sprintf("%s. The quick brown fox jumps over the lazy dog.", key)}
對於同步資料請求來說,必須等上一次的資料請求完畢才能發起下一次資料請求(原始的PHP即是如此),那麼如果100個0.2秒的資料請求,則最終耗時一定大於 20 秒。
下面是請求100次資料的處理:
// webdemo.go 同步資料請求func syncHandler(w http.ResponseWriter, r *http.Request) { timer := webdemo.NewTimer("sync") defer timer.End() page := webdemo.NewSyncPage() for i := 0; i < 100; i ++ { key := strconv.Itoa(i) page.SetContents(key) } page.Render(w)}
下面是擷取資料到頁面,並渲染頁面輸出到http.ResonseWriter的方法:
// sync.gofunc (page *SyncPage) SetContents(key string) { page.contents[key] = GetContents(key)}func (page *SyncPage) Render(w http.ResponseWriter) { lines := "" for i := 0; i < len(page.contents); i++ { key := strconv.Itoa(i) lines += fmt.Sprintf(TEMPLATE_LINE, page.contents[key]) } block := fmt.Sprintf(TEMPLATE_BLOCK, lines) fmt.Fprintf(w, TEMPLATE_PAGE, block)}
對於非同步來說,總時間長度略大於單條資料請求時間長度。下面是非同步請求的 handler 代碼,其實跟同步並無區別。
// webdemo.go 非同步Handlerfunc asyncHandler(w http.ResponseWriter, r *http.Request) { timer := webdemo.NewTimer("async") defer timer.End() page := webdemo.NewAsyncPage() page.CountOut = 100 for i:= 0; i < page.CountOut; i++ { key := strconv.Itoa(i) page.SetContents(key) } page.Render(w)}
為了實現非同步資料擷取,在 async.go 的代碼中,使用 go 語言強大的 channel 來實現。所以在 AsyncPage 結構中定義了一個 contents 是 chan。
// async.go// 頁面內容type AsyncContents struct { key, value string}// 頁面type AsyncPage struct { contents chan AsyncContents timeout chan bool CountOut int page Page}
在非同步頁面的 SetContents 方法中,用 go 關鍵字建立一個 goroutines 向 chan 中輸入內容。同時建立另一個 goroutines 作為 timeout(本例中不會出現 timeout 的情況,不過實際環境中這是必要的)。
// async.gofunc (page *AsyncPage) SetContents(key string) { // 非同步資料擷取 go func() { page.contents <- AsyncContents{key, GetContents(key)} }() // 設定針對頁面的逾時 go func() { time.Sleep(TIMEOUT) page.timeout <- true }()}
頁面的渲染也跟同步方式不同,通過 select 將資料從chan 中取出,並渲染到模板。
// async.gofunc (page *AsyncPage) Render(w http.ResponseWriter) { lines := "" LOOP: for i := 0; i < page.CountOut; i++{ select { case line := <-page.contents: lines = fmt.Sprintf("%s" + TEMPLATE_LINE, lines, line.value) // 每擷取一個資料,就去掉一個逾時 go func() {<-page.timeout}() case <-page.timeout: lines = fmt.Sprintf("%s" + TEMPLATE_LINE, lines, "Time Out") break LOOP } } block := fmt.Sprintf(TEMPLATE_BLOCK, lines) fmt.Fprintf(w, TEMPLATE_PAGE, block)}
為了示範非同步結構,我並沒有使用模板來渲染頁面。不過用模板來渲染也差不多:非同步擷取模板上叫做%VARn的變數,然後將資料集合渲染至模板上……
當資料的擷取有依賴關係的時候情況會比較複雜,不過如果把 web 頁面當作一個樹(DOM Tree?)的話,讓非同步資料從分葉節點開始擷取,逐層上推是個不錯的辦法。