[翻譯]飛翔的 gob

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

這個題目的原文叫做《Gobs on the wire》,作者巧妙的用了“gob”這個詞。gob本來是Golang的一個關於網路通訊協定的包。而在這裡,我感覺標題也可以翻譯為《關於線上的那一大陀……》。好吧,我得承認,這麼翻譯實在不雅。

————翻譯分割線————

飛翔的 gob

這周,我想跟大家談談如何用 Go 編寫基於同步請求和非同步事件通知的 Client/Server 系統。

為了協助學習 Go,我複製了一個Conserver 命令列服務。當然,現在對於世界來說沒必要有另外一個命令列服務。然而由於Go語言帶給開發人員的特性,非常適合用來做命令列服務,所以這將會是一次非常有趣的體驗。命令列服務的任務是從一個或者多個串口上匯總輸出(或者說各種系統上的一個或者多個實現了使用TCP串連的Rtelnet協議的終端服務)。 它將輸出記錄到檔案,同時讓輸出可以被別人即時的看到。它僅允許唯一一個使用者進行讀寫,用於實際控制裝置。

Go版本的命令列服務被稱作gocons。先去看看,然後回到這裡。跟以往一樣,我先說說在編寫這個的時候,我學到了Go的哪些東西。

最初,我嘗試用netchan來構建這個。我想,如果用戶端在某個通道(譯註:channel,下同)上向伺服器請求,當在某個命令列上有事件發生時接收通知這會是一件很酷的事情。但是,由於netchan不能處理多個通道,它徹底的崩潰了。 我認為,理論上是可能通過某種方法讓netchan處理多個通道,但是這看起來確實非常難, 而且多個通道可能永遠不同步。所以,我轉向了正確的方向……

接下來是想使用rpc包實現用戶端和伺服器之間的協議。但是,這帶來了另外一個問題:RPC,它的定義是同步的。它沒有明確定義服務端如何使用RPC包下發非同步事件到用戶端,如“這些位元組已經到達”。我需要自己編寫協議,通過兩個步驟,一個同步調用,例如“我可以收聽這個嗎?是的。”以及非同步事件“你監視的命令列剛剛輸出了這些位元組”。 我之前已經編寫了這些協議,這並不難,只是有一些繁瑣。而Go提供的gob包就是你開發一個協議所需要的全部。

而你需要做的就是描繪出協議訊息,並且讓gob負責處理打包和解包,就是計算訊息的分隔。在我們的例子中,這個類型被稱為connReq和connReply。在理想世界裡,這些應當是某個庫的公用類型,用戶端和伺服器端同時使用它們。在gocons中,我發懶了,只是複製並粘貼了它們。用戶端在net.TCPConn上有gob.Decode,而結果就是connReq(如果不是的話,那說明有什麼不對勁,用戶端可以幹掉串連或解碼這個串連上的後續內容)。由於Go沒有union(非型別安全的),connReq和connReply即便是給定的協議訊息不使用的情況下,也應當包含所有的欄位。我並沒有仔細思考這個協議, 但由於未使用欄位可能是位元組段或字串,不可能有很多;而且空的位元組段將被編碼為nil,而不是用零填充的整個位元組緩衝區。

對此更加精緻的設想是構造一個層次化的類型,最簡單的類型(僅有一個int指名類型)作為基礎,後面跟一個複雜的類型。但是要弄清楚給gob.Decode的是什麼類型是非常難的;這就像你必須將協議拆分為兩部分,並且分別對其調用go.Encode。第一個可以告訴這是什麼類型,而第二個gob可以是包含資料的短語。無論如何,我不會在gocons裡這麼做。簡單就好!

在伺服器中,有兩個程式碼片段很有趣。一個是使用JSON作為設定檔的格式。另一個是如何將新的資料發送到所有收聽者。

第一個比較簡單。僅僅是示範在不使用json.Unmarshall的情況下,如何從JSON檔案中擷取資料。我搞不清楚json.Unmarshall,所以在我嘗試不用它的情況下,讓json.Decode工作。我並不是說這麼做好,但是它能運行,這可能可以協助其他在Go中讀取JSON的正在尋找例子的人。

希望的輸入是這樣的:

{    "consoles": {        "firewall": "ts.company.com:2070",        "web01": "ts.company.com:2071",        "web02": "ts.company.com:2072",        "web03": "ts.company.com:2073"    }}

目標是在consoles中對應的每個索引值調用addConsole。

是這樣做的,如果你不希望這樣(或者知道如何做的話)使用json.Unmarshal吧:

  r, err := os.Open(*config, os.O_RDONLY, 0)  if err != nil {    log.Exitf("Cannot read config file %v: %v", *config, err)  }  dec := json.NewDecoder(r)  var conf interface{}  err = dec.Decode(&conf)  if err != nil {    log.Exit("JSON decode: ", err)  }  hash, ok := conf.(map[string]interface{})  if !ok {    log.Exit("JSON format error: got %T", conf)  }  consoles, ok := hash["consoles"]  if !ok {    log.Exit("JSON format error: key consoles not found")  }  c2, ok := consoles.(map[string]interface{})  if !ok {    log.Exitf("JSON format error: consoles key wrong type, %T", consoles)  }  for k, v := range c2 {    s, ok := v.(string)    if ok {      addConsole(k, s)    } else {      log.Exit("Dial string for console %v is not a string.", k)    }  }

這裡的模式大致如此,json.Decode提供了一個interface{},然後根據結構使用類型選取器,然後獲得你期望的在那裡獲得的內容。

更加簡單的辦法是使用json.Unmarshal。從文檔中很難理解如何使用,幸好這篇文章讓它看起來更加清晰。

伺服器是在一個迴圈裡處理i/o的一系列的goroutine組成。每個它監視的命令列擁有一個讀goroutine和一個寫goroutine。讀的從其中擷取位元組,然後向所有收聽的gocons用戶端分發。它管理了一個包含用戶端列表的鏈表,不過另外一種資料結構可能會工作得更好。不論是在net.TCPConn還是通道,用戶端都是沒有排序的。等待新資料的通道就像是用戶端的代理goroutine。當每個用戶端串連時,會建立一對goroutine,一個用於讀,一個用於寫。這允許我們在輸入上實現阻塞讀(例子可參看dec.Decode),而不用擔心阻塞伺服器上的其他任務。

利用一個獨立的goroutine保證負責向TCP串連寫入,這樣可以不使用任何鎖。作為練習,可以讓多個命令列管理器同時說:“我有一些資料需要TCP串連的多工!”而無須關心它們向串連寫入資料時相互幹擾。(當前的實現一次只可以監聽一個命令列。)

下面的片段示範了當有新的內容時,如何打包並且向所有命令列觀看者發送通知:

    select {      // a bunch of other channels to monitor here...      case data := <-m.dataCh:        if closed(m.dataCh) {          break L        }        // multicast the data to the listeners        for l := listeners; l != nil; l = l.next {          if closed(l.ch) {            // TODO: need to remove this node from the list, not just mark nil            l.ch = nil            log.Print("Marking listener ", l, " no longer active.")          }          if l.ch != nil {            ok := l.ch <- consoleEvent{data}            if !ok {              log.Print("Listener ", l, " lost an event.")            }          }        }

這樣,我們建立了一個新的consoleEvent,並且將其發送給了每個收聽者。這有點浪費:它產生了許多垃圾,這意味著記憶體回收行程需要更加努力的工作。可以建立一個consoleEvent,然後向所有的收聽者發送這一個。但是,如果這樣共用記憶體,需要開發人員決定是讓共用記憶體唯讀,還是使用互斥控制它的訪問。在我們的例子中,使用了唯讀方式,像這樣:

    // new event arrived from the console manager, so send it down the TCP connection    case ev := <-evCh:      reply.code = Data      reply.data = ev.data      err := enc.Encode(reply)      if err != nil {        log.Print("connection ", c.rw, ", failed to send data: ", err)        break L      }

在這個模式下,兩個goroutine只負責讀取和寫入,這就像魔法。這夢幻般的降低了實現gocons需要的代碼量。原先的命令列服務需要數百行複雜的代碼,關於設定select掩碼,等待select,檢測fd是否需要accept()或者read()或者其他(同時要找到讓fd可用的正確的資料結構)。在gocons中,以及其他Go程式,如http包實現的http服務,可以使用阻塞的讀,讓Go安排運行使得整個系統並未阻塞。

然而,如果考慮當向用戶端TCP串連寫入發生阻塞時的情況,會很有趣。當系統在寫時發生阻塞,它最終會放棄,並且阻止從命令列上讀取,阻塞其他所有用戶端。為了對應這種情況,你需要在適當的地方建立防火牆:共用的資源不應當讓個體阻塞它們。你需要在用戶端代理goroutine和命令列管理者讀取goroutine之間的通道設定一個隊列,讓其在通道上實現非阻塞寫,並且當一個阻塞了,就處理它。例如,可以關閉通道,然後說“嘿,你的排水不暢,應該清理一下,然後再來找我。”

用Go編寫並且調試這個伺服器,讓我學到了許多東西。而我仍然還有許多東西需要學習:代碼中仍然有一些神秘的東西,例如為什麼我需要runtime.Gosched()保證不會阻塞,以及如何處理關閉的通道在select中帶來的麻煩。還有更多的隱藏在setOwner中的神秘工作,首要的是:如何發現將從一個地方轉寄到另一個地方的“pump goroutine”中的bug(在Go運行時環境,或者我能理解的情況下)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.