大家好,我是公衆号「線下聚會遊戲」作者HullQin,開發了《聯機桌遊合集》,是個網頁,可以很友善的跟朋友聯機玩鬥地主、五子棋等遊戲。
背景
在專欄《Go WebSocket》裡,有一些前置文章:
第一篇文章:《為什麼我選用Go重構Python版本的WebSocket服務?》,介紹了我的目标。
第二篇文章:《你的第一個Go WebSocket服務: echo server》,介紹了一下怎麼寫一個WebSocket server。
第三篇文章:《單房間的聊天室》,介紹了如何實作一個單房間的聊天室。
第四篇文章:《多房間的聊天室(一)思考篇》,介紹了實作一個多房間的聊天室的思路。
第五篇文章:《多房間的聊天室(二)代碼實作》,介紹了實作一個多房間的聊天室的代碼。
第六篇文章:《多房間的聊天室(三)自動清理無人房間》,介紹了如何清理無人的房間,避免記憶體無限增長的問題。
第七篇文章:《多房間的聊天室(三)自動清理無人房間》,介紹了如何避免并發導緻的資源競争的問題,是通過悲觀鎖解決的。
溫馨提示:閱讀本文不需要閱讀前面的文章。但最好先讀完前三篇。
本文介紹了一個gorilla/websocket官方提供的簡易版的web shell案例。
代碼
見這裡: https://github.com/gorilla/websocket/blob/master/examples/command/main.go
體驗
go run main.go sh
然後浏覽器打開 127.0.0.1:8080,就可以輸入
ls
、
pwd
等指令體驗了。

但是這是個簡易的 Web Shell,是以不支援vim這種指令。隻能處理簡單的stdin和stdout。
另外,它有一個參數,剛才我們傳入的是
sh
,你也可以傳入其它可執行指令,例如
echo
,會開啟echo的互動指令。執行
go run main.go echo
後,在浏覽器内,你輸入什麼,它傳回什麼。
從main函數開始
閱讀一段go代碼,應該從外到裡,一層一層撥開她的衣。
var (
addr = flag.String("addr", "127.0.0.1:8080", "http service address")
cmdPath string
)
func main() {
flag.Parse()
if len(flag.Args()) < 1 {
log.Fatal("must specify at least one argument")
}
var err error
cmdPath, err = exec.LookPath(flag.Args()[0])
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/", serveHome)
http.HandleFunc("/ws", serveWs)
log.Fatal(http.ListenAndServe(*addr, nil))
}
flag.Parse()
是在處理參數,這裡要求必須有1個參數,後續會執行這個參數對應的指令,就可以在Web中互動了。
關于
exec.LookPath(flag.Args()[0])
:這是在環境變量中尋找PATH,傳回一個字元串(可能是絕對路徑或相對路徑)。
随後啟動了http服務(用
serveHome
處理),和websocket服務(用serveWs處理)。前者是展示html,後者處理websocket。
閱讀serveWs
建立ws連接配接
ws, err := upgrader.Upgrade(w, r, nil)
defer ws.Close()
上面是建立ws連接配接,以前聊過,不多說了。
建立用于标準輸出的Pipe
outr, outw, err := os.Pipe()
if err != nil {
internalError(ws, "stdout:", err)
return
}
defer outr.Close()
defer outw.Close()
Pipe returns a connected pair of Files; reads from r return bytes written to w. It returns the files and an error, if any.
上面是建立管道os.Pipe,之後用于連接配接标準輸出。
建立用于标準輸入的Pipe
inr, inw, err := os.Pipe()
if err != nil {
internalError(ws, "stdin:", err)
return
}
defer inr.Close()
defer inw.Close()
上面是建立管道os.Pipe,之後用于連接配接标準輸出。
讓作業系統執行PATH對應的指令(啟動了新的程序)
剛剛我們LookPath找到了具體要執行的指令,現在讓作業系統執行它,通過
os.StartProcess
:
proc, err := os.StartProcess(cmdPath, flag.Args(), &os.ProcAttr{
Files: []*os.File{inr, outw, outw},
})
if err != nil {
internalError(ws, "start:", err)
return
}
inr.Close()
outw.Close()
StartProcess會啟動一個程序,flag.Args()作為它的參數。
為了在Go這個程序中跟另一個程序互動,需要通過Pipe連接配接,就是我們剛才定義的2個。程式都有标準輸入、标準輸出、異常輸出,是以定義了
os.ProcAttr
,這裡我們把異常輸出也輸出到了标準輸出了。
啟動其它goroutine,處理輸入輸出
stdoutDone := make(chan struct{})
go pumpStdout(ws, outr, stdoutDone)
go ping(ws, stdoutDone)
pumpStdin(ws, inw)
pumpStdout處理程序的輸出;pumpStdin處理程序的輸入。ping隻是為了跟用戶端保持持久的連接配接。
stdoutDone
是程序結束的标志,結束後,ws連接配接也要斷開(畢竟連着也沒法互動,沒意義了)。
先閱讀簡單的ping
func ping(ws *websocket.Conn, done chan struct{}) {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil {
log.Println("ping:", err)
}
case <-done:
return
}
}
}
是個死循環,每隔一段時間(pingPeriod),都會主動個PingMessage,保持連接配接。浏覽器收到後會自動回複Pong消息。通過這種方式,雙方都知道彼此還連着。
當然,如果done了,程序結束,就可以停止ping了。相反也是一樣,ws斷開連接配接時,程序也可以結束了。
閱讀pumpStdin
注意,pumpStdin不是在serveWs用go開啟的goroutine。是以到這裡時,其實serveWs就阻塞在pumpStdin裡的死循環了。
func pumpStdin(ws *websocket.Conn, w io.Writer) {
defer ws.Close()
ws.SetReadLimit(maxMessageSize)
ws.SetReadDeadline(time.Now().Add(pongWait))
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := ws.ReadMessage()
if err != nil {
break
}
message = append(message, '\n')
if _, err := w.Write(message); err != nil {
break
}
}
}
主要就是讀取
ws
消息,然後把消息寫入
w
(即
inw
這個Pipe),之後,上面說的新啟動的程序會收到這個消息。
閱讀pumpStdout
func pumpStdout(ws *websocket.Conn, r io.Reader, done chan struct{}) {
s := bufio.NewScanner(r)
for s.Scan() {
ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.TextMessage, s.Bytes()); err != nil {
ws.Close()
break
}
}
if s.Err() != nil {
log.Println("scan:", s.Err())
}
close(done)
ws.SetWriteDeadline(time.Now().Add(writeWait))
ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
time.Sleep(closeGracePeriod)
ws.Close()
}
新啟動的程序有标準輸出或異常輸出時,會發送到Pipe,我們代碼中通過
outr
可擷取到輸出,即本函數的參數
r
。