不知道大家还记得不记得大约一年前,我的一个白日梦《关于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