用Go实现异步的Web开发

不知道大家还记得不记得大约一年前,我的一个白日梦《关于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?)的话,让异步数据从叶节点开始获取,逐层上推是个不错的办法。

Join the Conversation

6 Comments

Leave a comment

Your email address will not be published. Required fields are marked *