不知道大家还记得不记得大约一年前,我的一个白日梦《关于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 run 8g -o _go_.8 timer.go data.go page.go async.go sync.go rm -f _obj/webdemo.a gopack grc _obj/webdemo.a _go_.8 cp _obj/webdemo.a "/home/mikespook/bin/go/pkg/linux_386/webdemo.a" 8g webdemo.go 8l -o webdemo webdemo.8 ./webdemo 2011/03/25 15:06:35 async: 228623000 2011/03/25 15:06:57 sync: 20024992000
核心思路很简单,我在模拟数据请求的代码里用了 time.Sleep(),让每次数据请求都延迟 0.2 秒(一个优化得不太好的,很大的数据库表的一次很烂的查询所用时间):
// data.go func 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.go func (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 异步Handler func 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.go func (page *AsyncPage) SetContents(key string) { // 异步的数据获取 go func() { page.contents <- AsyncContents{key, GetContents(key)} }() // 设置针对页面的超时 go func() { time.Sleep(TIMEOUT) page.timeout <- true }() }
页面的渲染也跟同步方式不同,通过 select 将数据从chan 中取出,并渲染到模板。
// async.go func (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?)的话,让异步数据从叶节点开始获取,逐层上推是个不错的办法。
Leave a Reply