深入 Go Playground 内幕
简介2010年9月,我们介绍了Go Playground,这是一个完全由Go代码组成和返回程序运行结果的web服务器。 概览
后端后端程序本身很简单,所以这里我们不讨论它的实现。有趣的部分是我们如何在一个安全环境下安全地执行任意用户代码,于此同时还提供如时间、网络及文件系统等的核心功能。 (这个特殊的工具将合并到Go 1.3中。想了解更多,阅读设计文档。如果你想提前体验NaCl,你可以检出一个包含所有变更的分支。) 本地客户端会限制程序占用CPU和RAM的使用量,此外还会阻止程序访问网络和文件系统。然而这会导致一个问题,Go程序的许多关键优势,比如并发和网络访问。此外访问文件系统,对于许多程序也是至关重要的。我们需要时间功能,才展现高效的并发性能。显然我们需要网络和文件系统,才能显示出来访问网络和文件系统方面的优势。 伪时间playground里面的程序可用CPU时间和内存都是有限的。除此以外程序实际使用时间也是有限制的。这是因为每个运行在playground的程序都消耗着后台资源,以及占据客户端和后台间的基础设施。限制每个程序的运行时间让我们的维护更加可遇见,而且可以保护我们免受拒绝服务攻击。 通过使用一个高明的小把戏,我们可以使得Go程序认为它是在休眠,而实际上这个休眠没有花费任何时间。在介绍这个小把戏之前,我们需要了解调度程序是管理goroutine的休眠的原理。 playground运行时版本中维护着一个内部时钟。当修改后的调度器检测到一个死锁,那么它将检查是否有一些挂起的计时器。如果有的话,它会将内部时钟的时间调整到最早计时器的促发时间,然后唤醒goroutine计时器。这样一直循环往复,程序都认为时间过去了,而实际上休眠几乎没有耗时。 下面的程序每秒输出当前时间,然后三秒后退出.试着运行一下。 func main() { stop := time.After(3 * time.Second) tick := time.NewTicker(1 * time.Second) defer tick.Stop() for { select { case <-tick.C: fmt.Println(time.Now()) case <-stop: return } } } 这是如何做到的? 这其实是后台、前端和客户端合作的结果。 playground的运行环境包提供了一个在每个写入数据之前引入一个小“回放头”的特殊写函数,它。回放头中包含一个逻辑字符,当前时间,要写入数据长度。一个写操作的回放头结构如下: 0 0 P B <8-byte time> <4-byte data length> <data> 这个程序的原始输出类似这样: x00x00PBx11x74xefxedxe6xb3x2ax00x00x00x00x1e2009-11-10 23:00:01 +0000 UTC x00x00PBx11x74xefxeex22x4dxf4x00x00x00x00x1e2009-11-10 23:00:02 +0000 UTC x00x00PBx11x74xefxeex5dxe8xbex00x00x00x00x1e2009-11-10 23:00:03 +0000 UTC 前端将这些输出解析为一系列事件并返回给客户端一个事件列表的JSON对象: { "Errors": "","Events": [ { "Delay": 1000000000,"Message": "2009-11-10 23:00:01 +0000 UTCn" },{ "Delay": 1000000000,"Message": "2009-11-10 23:00:02 +0000 UTCn" },"Message": "2009-11-10 23:00:03 +0000 UTCn" } ] } JavaScript客户端(在用户的Web浏览器中运行的)然后使用提供的延迟间隔回放这个事件。对用户来说看起来程序是在实时运行。 伪文件系统在Go本地客户端(NaCl)的工具链上构建的程序,是不能访问本地机器的文件系统的。为了解决这个问题syscall包中有个文件访问的函数(Open,Read,Write等等)都是操作在一个内存文件系统上的。这个内存文件系统是由syscall包自身实现的。既然syscall包是一个Go代码与操作系统内存间的一个接口,那么用户程序会将这个伪文件系统会和一个真实的文件系统一个样看待。 func main() { const filename = "/tmp/file.txt" err := ioutil.WriteFile(filename,[]byte("Hello,file systemn"),0644) if err != nil { log.Fatal(err) } b,err := ioutil.ReadFile(filename) if err != nil { log.Fatal(err) } fmt.Printf("%s",b) } 当一个进程开始,这个伪文件系统加入/dev目录下的设备和一个/tmp空目录。那么程序可以对这个文件系统和平常一样进行操作,但是进程退出后,所有对文件系统的改变将会丢失 在初始化的时候,可以上传zip压缩文件(详见unzip_nacl.go)迄今为止只会在进行标准库测试的时候,我们会使用解压缩工具来提供测试数据文件。可是我们打算playground程序可以运行文档示例、博客帖子和Golang的教程里面的数据。 func Open(path string,openmode int,perm uint32) (fd int,err error) { fs.mu.Lock() defer fs.mu.Unlock() f,err := fs.open(path,openmode,perm&0777|S_IFREG) if err != nil { return -1,err } return newFD(f),nil } 文件描述符被一个称为files的全局片段记录着。每个文件描述符对应着一个file,而且每个file都会提供一 fileImpl接口的实现。这里有几个接口的实现: 伪网络访问和文件系统一样,playground的网络堆栈是由syscall包在进程内部模拟出来的,这可以让playground项目使用回送地址(127.0.0.1)。但不能请求其他主机。 运行下面可执行的实例代码。这个程序首先会监听TCP的端口,接着等待连接的到来,然后将连接传来的数据复制到标准输出,最后程序退出。在另外一个goroutine中,他会连接那个监听中的端口,然后向连接里面写入数据,最后关闭。 func main() { l,err := net.Listen("tcp","127.0.0.1:4000") if err != nil { log.Fatal(err) } defer l.Close() go dial() c,err := l.Accept() if err != nil { log.Fatal(err) } defer c.Close() io.Copy(os.Stdout,c) } func dial() { c,err := net.Dial("tcp","127.0.0.1:4000") if err != nil { log.Fatal(err) } defer c.Close() c.Write([]byte("Hello,networkn")) } 网络的接口比文件要复杂的多,所以伪网络的接口的实现会比伪文件系统的要庞大和复杂的多。伪网络必须模拟读和写的超时,以及处理不同地址类型和协议等等。 前端playground的前端是另外一个简单的程序 (不到100行). 它的主要功能是接受客户端的HTTP请求,然后向后台发出对应的RPC请求,同时还会完成一些缓存工作。 客户端各种使用playground的站点,共享着一些同样的Javascript代码来搭建用户访问接口(代码窗口和输出窗口,运行按钮等等),通过这些接口来后playground前端交互。 离线运行不管是Go Tour还是Present Tool都可以离线运行。 这样的离线功能对于访问网络有限制的人们来说,实在太棒了。 其他客户端playground服务不单单只有为了给Go项目官方使用 (Go by Example是另外一个例子) 。我们很高兴你能在你的站点使用该服务。我们唯一的要求就是您事先和我们联系,在您的请求中使用唯一用户代理(这样我们可以确认您的身份),此外您提供的服务是有益于Go社区的。 结束语不论是godoc,是tour,还是这样的blog,playground已经成为Go文档系列中不可或缺的一部分了。随着最近的伪文件系统和伪网络堆栈的引入,我们将激动地完善我们的学习资料来覆盖这些新内容。 这篇文章是12月12号的Go Advent Calendar中的一篇,Go AdventCalendar是一系列的博客帖子集合。 作者 Andrew Gerrand 相关文章
原文:Inside the Go Playground (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |