Go中包含有CGI包,net/http/cgi,這篇文章就是來閱讀和使用這個包。關於cgi的參數和運行,可以看這篇文章:CGI的一些知識點
CGI包閱讀
cgi包的存在就告訴我們一件事情,cgi服務端和客戶端完全可以使用Go來寫
這個包其實很簡單,只有兩個文件,其他都是測試程序
child.go
host.go
host.go是可以直接宿主到go的web服務器上的代碼,里面提供了對request和response的直接處理函數ServerHTTP, 當你是使用go的http包寫了個http之后,就可以使用ServerHTTP對請求直接配置上cgi,有點像apache中自帶了php-cgi
child.go則是已經進入到腳本子進程中了,如果你的CGI腳本是go代碼生成的可執行腳本,那么你就會有用到這個文件里面的函數了。這個文件內提供了將命令行環境(CGI請求)轉換成Go的http包中的request的方法。
host.go是cgi的啟動父程序需要用到的包,child.go是子程序需要用到的包
先看host.go
首先是trailingPort,這個變量是cgi服務器監聽的端口號,(比如在nginx中我們一般都監聽9000)
然后是osDefaultInheritEnv,這個map將各個平台的共享庫默認路徑列出來了。為什么設置這個變量呢?這樣說,由於cgi服務器執行命令的時候命令查找設置參數有的是去環境變量中獲取的,因此對每個命令執行需要設置一下環境變量。而在不同的平台,動態庫的路徑是不一樣的,所以有了這么個Map。
Handler是在子程序中執行cgi腳本的。里面要注意的結構是兩個Env和InheritEnv兩個,一個是特別設定的環境變量,另外一個是繼承的環境變量。
還有Handler中的Path,就是執行文件的路徑,比如/test.php
下面就是最重要的ServeHTTP了,這個是用來回調處理HTTP請求的,它會將HTTP請求轉化為CGI請求,並且執行這個cgi腳本。
在這個函數中,能看到CGI的RFC標准參數賦值,然后可以看到拼出了env之后將env作為exec.Cmd的Env來調用cgi腳本(path)。同時也看到了當body內有content的時候,會將Body作為stdin輸入,然后從stdout出來的東西逐行讀取,然后讀取到header和body中去。
看了host.go的實現就很好理解child.go的實現了。
從Serve(handler)來看,先是使用將nginx提供的cgi請求轉換成為net包中的http request和response,如果你有設置handler,就用request和response來進行處理。
后續的幾個操作Write,Flush都已經是簡單的buffer處理了。
CGI包使用
然后自然想到的一個問題,能不能實現
go-web服務器 + go-cgi + cgi-script
這個是可以做的,而且也不復雜:
代碼如下:
package main import( "net/http/cgi" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){ handler := new(cgi.Handler) handler.Path = "/home/yejianfeng/go/gopath/src/cgi-script/" + r.URL.Path log.Println(handler.Path) handler.Dir = "/home/yejianfeng/go/gopath/src/cgi-script/" handler.ServeHTTP(w, r) }) log.Fatal(http.ListenAndServe(":8989",nil)) }
如果你在cgi-script中有個可運行的cgi腳本,比如test.perl
那么我們就可以在瀏覽器中調用http://10.16.15.64:8989/test.perl
來進行腳本調用
然后進一步想,能不能把go代碼當作是php這樣的動態腳本來運行呢,這樣就可以一邊修改go源碼,一邊就可以在頁面中立刻顯示修改結果了。即
go-web + go-cgi + go-cgi-script?
答案同樣也是可以,但是這個時候由於xx.go並非是可執行文件,只能使用go run 來進行調用。
代碼:
package main import( "net/http/cgi" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){ handler := new(cgi.Handler) handler.Path = "/home/yejianfeng/bin/go" script := "/home/yejianfeng/go/gopath/src/cgi-script/" + r.URL.Path log.Println(handler.Path) handler.Dir = "/home/yejianfeng/go/gopath/src/cgi-script/" args := []string{"run", script} handler.Args = append(handler.Args, args...) handler.Env = append(handler.Env, "GOPATH=/home/yejianfeng/go/gopath") handler.Env = append(handler.Env, "GOROOT=/home/yejianfeng/go/go") log.Println(handler.Args) handler.ServeHTTP(w, r) }) log.Fatal(http.ListenAndServe(":8989",nil)) }
然后在cgi-script文件夾中建立test.go
package main import( "fmt" ) func init() { fmt.Print("Content-Type: text/plain;charset=utf-8\n\n"); } func main() { fmt.Println("This is go test!!!!") }
這里的init()是必須打印出來的
然后就可以打印出頁面了
這時候go就像php腳本一樣,如果你修改了test.go就能立刻在頁面上顯示出來了
對於第二種,當然也可以使用監控文件夾的修改等,當go項目修改后就重新build出一個可執行腳本,然后cgi的handler就修改下路由指向到這個可執行腳本。
關於監控文件的項目現在已經有很多開源的了:比如這個https://github.com/jianfengye/go-superviser
Ps: 文中說的例子已經加到http://go.funaio.com/pkg/net/http/cgi/ 的Example中了。