2010年9月,咱們介紹了Go Playground,這是一個徹底由Go代碼組成和返回程序運行結果的web服務器。
若是你是一位Go程序員,那你極可能已經經過閱讀Go教程或執行Go文檔中的示例程序的途徑使用過Go Playground了。
你也能夠經過點擊 talks.golang.org上幻燈片中的「Run」 按鈕或某個博客上的程序(好比最近一篇關於字符串的blog)而使用之.
本文咱們將學習Go playground是如何實現並與其它服務整合的。其實現涉及到不一樣的操做系統和運行時間,這裏咱們假設你們用來編寫Go的系統都基本相同。前端
playground服務有三部分:
* 一個運行於Google服務之上的後端。它接收RPC請求,使用gc工具編譯用戶程序,執行,並將程序的輸出(或編譯錯誤)做爲RPC響應返回。
* 一個運行在 GAE上的前端。它接收來自客戶端的HTTP請求並生成相應的RPC請求到後端。它也作一些緩存。
* 一個JavaScript客戶端實現的用戶界面,並生成到前端的HTTP請求。程序員
後端程序自己很簡單,因此這裏咱們不討論它的實現。有趣的部分是咱們如何在一個安全環境下安全地執行任意用戶代碼,於此同時還提供如時間、網絡及文件系統等的核心功能。
爲從Google的基礎設施隔離用戶程序,後端將它們運行在原生客戶端(或「NaCl」)中,原生客戶端(NaCl)—一個Google開發的技術,容許x86程序在Web瀏覽器中安全執行。後端使用一個能生成NaCl可執行文件的特殊版gc工具。golang
(這個特殊的工具將合併到Go 1.3中。想了解更多,閱讀設計文檔。若是你想提早體驗NaCl,你能夠檢出一個包含全部變動的分支。)web
本地客戶端會限制程序佔用CPU和RAM的使用量,此外還會阻止程序訪問網絡和文件系統。然而這會致使一個問題,Go程序的許多關鍵優點,好比並發和網絡訪問。此外訪問文件系統,對於許多程序也是相當重要的。咱們須要時間功能,才展示高效的併發性能。顯然咱們須要網絡和文件系統,才能顯示出來訪問網絡和文件系統方面的優點。
儘管如今這些功能都被支持了,可是2010年發佈的初版playground時,沒有一項被支持的。當前時間功能是在2009年11月10的被支持的,但是 time.Sleep 卻不能使用,並且多數與系統和網絡有關的包都不被支持的
一年後,咱們在playground上面實現了一個僞時間,這才使得程序能夠有個正確的休眠行爲。較新的playground更新引入了僞網絡和僞文件系統,這使得playground的工具鏈與正常的Go工具鏈相同。這些新引入的功能會在下面具體闡述。c#
playground裏面的程序可用CPU時間和內存都是有限的。除此之外程序實際使用時間也是有限制的。這是由於每一個運行在playground的程序都消耗着後臺資源,以及佔據客戶端和後臺間的基礎設施。限制每一個程序的運行時間讓咱們的維護更加可碰見,並且能夠保護咱們免受拒絕服務攻擊。
可是當程序使用時間功能函數的時候,這些限制將變得很是不合適。在 Go Concurrency Patterns 講話中經過一個例子來演示這個糟糕的問題。這是一個使用時間功能函數好比 time.Sleep 和time.After的例子程序,當運行在早期的playground中時,這些程序的休眠會失效並且行爲很奇怪(有時甚至出現錯誤)segmentfault
經過使用一個高明的小把戲,咱們可使得Go程序認爲它是在休眠,而實際上這個休眠沒有花費任什麼時候間。在介紹這個小把戲以前,咱們須要瞭解調度程序是管理goroutine的休眠的原理。
當一個goroutine調用time.Sleep(或者其餘類似函數),調度器會在掛起的計時器堆中添加中增長一個計時器,並讓goroutine休眠。在這期間,一個特殊的goroutine計算器管理着這個堆。當這個特殊的goroutine計算器開始工做時,首先,它告訴調度器,當堆中的下一個掛起的計時器準備計時的時候喚醒本身,而後它本身就開始休眠了。當這個特殊計時器被喚醒後首先是檢測是否有計時器超時了,若是有那麼就喚醒相應的goroutine,而後又回到休眠狀態。
明白了這個原理後,那個小把戲只是改變喚醒goroutine的計時器的條件。調度器並非通過一段時間後進行喚醒,並且僅僅等待一個全部goroutines 都阻塞的死鎖產生後就進行喚醒。後端
playground運行時版本中維護着一個內部時鐘。當修改後的調度器檢測到一個死鎖,那麼它將檢查是否有一些掛起的計時器。若是有的話,它會將內部時鐘的時間調整到最先計時器的促發時間,而後喚醒goroutine計時器。這樣一直循環往復,程序都認爲時間過去了,而實際上休眠幾乎沒有耗時。
這些調度器的改變細節詳見 proc.c 和 time.goc。
僞時間解決了後臺資源耗盡的問題,可是程序的輸出該怎麼辦呢?看見一個在休眠的程序,卻幾乎不耗時地正確完成工做了,這是得多麼的奇怪啊!瀏覽器
下面的程序每秒輸出當前時間,而後三秒後退出.試着運行一下。緩存
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>
這個程序的原始輸出相似這樣:
\x00\x00PB\x11\x74\xef\xed\xe6\xb3\x2a\x00\x00\x00\x00\x1e2009-11-10 23:00:01 +0000 UTC \x00\x00PB\x11\x74\xef\xee\x22\x4d\xf4\x00\x00\x00\x00\x1e2009-11-10 23:00:02 +0000 UTC \x00\x00PB\x11\x74\xef\xee\x5d\xe8\xbe\x00\x00\x00\x00\x1e2009-11-10 23:00:03 +0000 UTC
前端將這些輸出解析爲一系列事件並返回給客戶端一個事件列表的JSON對象:
{ "Errors": "", "Events": [ { "Delay": 1000000000, "Message": "2009-11-10 23:00:01 +0000 UTC\n" }, { "Delay": 1000000000, "Message": "2009-11-10 23:00:02 +0000 UTC\n" }, { "Delay": 1000000000, "Message": "2009-11-10 23:00:03 +0000 UTC\n" } ] }
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 system\n"), 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的教程裏面的數據。
具體實現詳見 fs_nacl.go 和 fd_nacl.go 文件(因爲是_nacl的後綴,因此只有當GOOS被設置爲nacl時候,這些文件纔會被加入到syscall包中)。
這個僞文件系統由 fsys struct 表明。其中一個全局實例(稱爲fs)在初始化的時候被建立。各類和文件有關的函數都操做在fs上,而不是進行真實的系統調用。例如,這裏有個 syscall.Open函數:
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接口的實現。這裏有幾個接口的實現:
* fsysFile表明常規文件和設備 (such as/dev/random) ,
* 標準輸入輸出和標準錯誤都是naclFile的實例,這可使用系統調用來操做真實文件(這是playground中的程序惟一訪問外部環境的途徑,
* 網絡套接字有着本身的實現,下面章節中會討論.
和文件系統同樣,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, network\n")) }
網絡的接口比文件要複雜的多,因此僞網絡的接口的實現會比僞文件系統的要龐大和複雜的多。僞網絡必須模擬讀和寫的超時,以及處理不一樣地址類型和協議等等。
具體實現詳見net_nacl.go。推薦從netFile開始閱讀,由於這是網絡套接字對於fileImpl接口的實現。
playground的前端是另一個簡單的程序 (不到100行). 它的主要功能是接受客戶端的HTTP請求,而後向後臺發出對應的RPC請求,同時還會完成一些緩存工做。
前端提供一個HTTP處理程序,詳見http://golang.org/compile。這個處理程序接受帶有body標籤(其中包含要運行的Go程序代碼)和一個可選version標籤(多數客戶端應該是‘2’)的POST請求。
當前端收到一個HTTP編譯請求的時候,它首先查看緩存,檢查以前是否有過一樣的編譯請求。若是發現存在同,那麼就會將緩存的響應直接返回。緩存能夠防止像Go主頁上那樣的大衆化程序讓後臺過載。若是發現該請求以前沒有被緩存過,那麼前端會向後臺發出相應的RPC請求,而後緩存後臺的響應,接着分析對應的事件回放(詳見僞時間),最後經過HTTP響應將JSON格式的對象返回到客戶端(像上面描述那樣)。
各類使用playground的站點,共享着一些一樣的Javascript代碼來搭建用戶訪問接口(代碼窗口和輸出窗口,運行按鈕等等),經過這些接口來後playground前端交互。
具體實如今go.tool資源庫的playground.js文件中,能夠經過go.tools/godoc/static包來導入。 其中一些代碼較爲簡潔,也有一些比較繁雜, 由於這是由幾個不一樣的客戶端代碼合併出來的。
playground函數使用一些HTML元素,而後構成一個交互式的playground窗口小部件。若是你想將playground添加到你的站點的話,你就可使用這些函數。
Transport接口 (非正式的定義, 是JavaScript腳本)的設計是依據網站前端交互方式提。 HTTPTransport是一個Transport的實現,能夠發送如前描述的以HTTP爲基礎的協議。 SocketTransport是另一個實現,發送WebSocket (詳見下面的'Playing offline')。
爲了遵照[同源策略](http://en.wikipedia.org/wiki/Same-origin_policy),各類網站服務器(例如godoc)經過playground在http://golang.org/compile下的服務來完成代理請求。這個代理是經過共有的 go.tools/playground 包來完成的。
不論是Go Tour仍是Present Tool均可以離線運行。 這樣的離線功能對於訪問網絡有限制的人們來講,實在太棒了。
爲了離線運行,這些工具在本地運行一個特殊版本的playground後端。這個特殊的後端使用的是常規GO
工具,這些工具沒有上面提到的那些修改,並且使用WebSocker來與客戶端進行通訊。
WebSocket的後端實現詳見go.tools/playground/socket包。在Inside Present講話中討論了代碼細節。
playground服務不僅僅只有爲了給Go項目官方使用 (Go by Example是另一個例子) 。咱們很高興你能在你的站點使用該服務。咱們惟一的要求就是您事先和咱們聯繫,在您的請求中使用惟一用戶代理(這樣咱們能夠確認您的身份),此外您提供的服務是有益於Go社區的。
不管是godoc,是tour,仍是這樣的blog,playground已經成爲Go文檔系列中不可或缺的一部分了。隨着最近的僞文件系統和僞網絡堆棧的引入,咱們將激動地完善咱們的學習資料來覆蓋這些新內容。
可是,最後,playground只是冰山一角,隨着本地客戶端(Native Client)將要支持Go1.3,咱們期盼着社區作出更棒的功能。
這篇文章是12月12號的Go Advent Calendar中的一篇,Go AdventCalendar是一系列的博客帖子集合。
做者 Andrew Gerrand
原文:Inside the Go Playground
轉載自:開源中國社區--Mitisky, Garfielt, cmy00cmy, JAVA草根