這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
現代網頁應用程式正日趨豐富而複雜。像這樣有趣又有活力的體驗很受使用者歡迎。使用者無需向伺服器發起調用,或重新整理瀏覽器,就可以讓頁面即時更新。早期的開發人員依賴 AJAX 來建立具備近乎即時體驗的應用程式。而現在,他們運用 WebSockets 就能建立完全即時的應用程式了。
本教程中我們將使用 Go 程式設計語言以及 WebSockets 來建立一個即時的聊天應用程式。前端將會使用 HTML5 和 VueJS 來編寫。該內容需要你對 Go 語言, JavaScript 以及 HTML5 有一個基礎的瞭解,最好有一點點使用 VueJS 的經驗。
如需使用 Go,你可以看看 Go 的官方網站上優秀的互動式教程:
https://tour.golang.org/welcome/1
如需使用 Vue,你可以看看 Jeffrey Way 在 Laracasts 上提供的優秀的系列視頻教程:
https://laracasts.com/series/learn-vue-2-step-by-step
leoxu 翻譯於 5個月前 0人頂 頂 翻譯得不錯哦!
WebSocket 是什麼?
通常 Web 應用程式使用一個或多個請求對 HTTP 伺服器提供對外服務。用戶端軟體通常是 網頁瀏覽器向伺服器發送請求,伺服器發回一個響應。響應通常是 HTML 內容,由瀏覽器來渲染為頁面。樣式表,JavaScript 代碼和映像也可以在響應中發送回來以完成整個網頁。每個請求和響應都屬於特定的單獨的串連的一部分,像 Facebook 這樣的大型網站為了渲染單個頁面實際上可以產生數百個這樣的串連。
AJAX 的工作方式跟這個完全相同。使用 JavaScript,開發人員可以向 HTTP 伺服器請求一小段資訊,然後根據響應更新部分頁面。這可以在不重新整理瀏覽器的情況下完成,但仍然存在一些限制。
Tocy 翻譯於 5個月前 2人頂 頂 翻譯得不錯哦!
每個 HTTP 要求/響應的串連在被響應之後都會關閉,因此獲得任何新的資訊必須建立另一個串連。如果沒有新的請求發送給伺服器,它就不知道用戶端正在尋找新的資訊。能讓 AJAX 應用程式看起來像即時的一種技術是定時迴圈發送 AJAX 請求。在設定了時間間隔之後,應用程式可以重新將請求發送到伺服器,以查看是否有任何更新需要反饋給瀏覽器。這比較適合小型應用程式,但並不高效。這時候 WebSockets 就派上用場了。
WebSockets 是由 網際網路工程任務推動小組(IETF)建立的建議標準的一部分。 RFC6455 中詳細描述了 WebSockets 實現的完整技術規範。下面是該文檔定義 WebSocket 的節選:
WebSocket 通訊協定用於用戶端代碼和遠程主機之間進行通訊,其中用戶端代碼是在可控環境下的非授信代碼
換句話說,WebSocket 是一個總是開啟的串連,允許用戶端和伺服器自發地來回傳送訊息。伺服器可在必要時將新資訊推送到用戶端,用戶端也可以對伺服器執行相同操作。
Tocy 翻譯於 5個月前 0人頂 頂 翻譯得不錯哦!
JavaScript 中的 WebSockets
大多數現代瀏覽器都在其 JavaScript 實現中支援 WebSockets。要從瀏覽器中啟動一個 WebSocket 串連,你可以使用簡單的 WebSocket JavaScript 對象,如下:
var ws = new Websocket("ws://example.com/ws");
您唯一需要的參數是一個 URL,WebSocket 串連可通過此 URL 串連伺服器。該請求實際是一個 HTTP 要求,但為了安全連線我們使用“ws://”或“wss://”。這使伺服器知道我們正在嘗試建立一個新的 WebSocket 串連。之後伺服器將“升級”該用戶端和服務之間的串連到永久的雙向串連。
一旦新的 WebSocket 對象被建立,並且串連成功建立之後,我們就可以使用“send()”方法發送文本到伺服器,並在 WebSocket 的“onmessage”屬性上定義一個處理函數來處理從伺服器發送的訊息。具體邏輯會在之後的聊天應用程式代碼中解釋。
Tocy 翻譯於 5個月前 0人頂 頂 翻譯得不錯哦!
Go 中的 WebSockets
WebSockets 並不包含在 Go 標準庫中,但幸運的是有一些不錯的第三方包讓 WebSockets 的使用輕而易舉。在這個例子中,我們將使用一個名為“gorilla/websocket”的包,它是流行的 Gorilla Toolkit 包集合的一部分,多用於在 Go 中建立 Web 應用程式。請運行以下命令進行安裝:
$ go get github.com/gorilla/websocket
Tocy 翻譯於 5個月前 0人頂 頂 翻譯得不錯哦!
構建伺服器
這個應用程式的第一部分是伺服器。這是一個處理請求的簡單 HTTP 伺服器。它將為我們提供 HTML5 和 JavaScript 代碼,以及建立用戶端的 WebSocket 串連。另外,伺服器還將跟蹤每個 WebSocket 串連並通過 WebSocket 串連將聊天資訊從一個用戶端發送到所有其他用戶端。首先建立一個新的空目錄,然後在該目錄中建立一個“src”和“public”目錄。在“src”目錄中建立一個名為“main.go”的檔案。
搭建伺服器首先要進行一些設定。我們像所有 Go 應用程式一樣啟動應用程式,並定義包命名空間,在本例中為“main”。接下來我們匯入一些有用的包。 “log”和“net/http”都是標準庫的一部分,將用於日誌記錄並建立一個簡單的 HTTP 伺服器。最終包“github.com/gorilla/websocket”將協助我們輕鬆建立和使用 WebSocket 串連。
package mainimport ( "log" "net/http" "github.com/gorilla/websocket")
Tocy 翻譯於 5個月前 0人頂 頂 翻譯得不錯哦!
下面的兩行代碼是一些全域變數,在應用程式的其它地方會被用到。全域變數的實踐較差,不過這次為了簡單起見我們還是使用了它們。第一個變數是一個 map 映射,其鍵對應是一個指向 WebSocket 的指標,其值就是一個布爾值。我們實際上並不需要這個值,但使用的映射資料結構需要有一個映射值,這樣做更容易添加和刪除單項。
第二個變數是一個用於由用戶端發送訊息的隊列,扮演通道的角色。在後面的代碼中,我們會定義一個 goroutine 來從這個通道讀取新訊息,然後將它們發送給其它串連到伺服器的用戶端。
var clients = make(map[*websocket.Conn]bool) // connected clientsvar broadcast = make(chan Message) // broadcast channel
接下來我們建立一個 upgrader 的執行個體。這隻是一個對象,它具備一些方法,這些方法可以擷取一個普通 HTTP 連結然後將其升級成一個 WebSocket,稍後會有相關代碼介紹。
// Configure the upgradervar upgrader = websocket.Upgrader{}
最後我們將定義一個對象來管理訊息,資料結構比較簡單,帶有一些字串屬性,一個 email 地址,一個使用者名稱以及實際的訊息內容。我們將利用 email 來展示 Gravatar 服務所提供的唯一身份標識。
leoxu 翻譯於 5個月前 1人頂 頂 翻譯得不錯哦!
由反引號包含的文本是 Go 在對象和 JSON 之間進行序列化和還原序列化時需要的中繼資料。
// Define our message objecttype Message struct { Email string `json:"email"` Username string `json:"username"` Message string `json:"message"` }
Go 應用程式的主要入口總是 "main()" 函數。代碼非常簡潔。我們首先建立一個靜態檔案服務,並將之與 "/" 路由綁定,這樣使用者訪問網站時就能看到 index.html 和其它資源。在這個樣本中我們有一個儲存 JavaScript 代碼的 "app.js" 檔案和一個儲存樣式的 "style.css" 檔案。
func main() { // Create a simple file server fs := http.FileServer(http.Dir("../public")) http.Handle("/", fs)
我們想定義的下一個路由是 "/ws",在這裡處理啟動 WebSocket 的請求。我們先向處理函數傳遞一個函數的名稱,"handleConnections",稍後再來定義這個函數。
func main() { ... // Configure websocket route http.HandleFunc("/ws", handleConnections)
下一步就是啟動一個叫 "handleMessages" 的 Go 程式。這是一個並行過程,獨立於應用和其它部分運行,從廣播頻道中取得訊息並通過各用戶端的 WebSocket 串連傳遞出去。並行是 Go 中一項強大的特性。關於它如何工作的內容超出了這篇文章的範圍,不過你可以自行查看 Go 的官方教程網站。如果你熟悉 JavaScript,可聯想一下並行過程,作為後台過程啟動並執行 Go 程式,或 JavaScript 的非同步函數。
func main() { ... // Start listening for incoming chat messages go handleMessages() 邊城 翻譯於 5個月前 1人頂 頂 翻譯得不錯哦!
最後,我們向控制台列印一個輔助資訊並啟動 Web 服務。如果有錯誤發生,我們就把它記錄下來然後退出應用程式。
func main() { ... // Start the server on localhost port 8000 and log any errors log.Println("http server started on :8000") err := http.ListenAndServe(":8000", nil) if err != nil { log.Fatal("ListenAndServe: ", err) }}
接下來我們建立一個函數處理傳入的 WebSocket 串連。首先我們使用升級的 "Upgrade()" 方法改變初始的 GET 請求,使之成為完全的 WebSocket。如果發生錯誤,記錄下來,但不退出。同時注意 defer 語句,它通知 Go 在函數返回的時候關閉 WebSocket。這是個不錯的方法,它為我們節省了不少可能出現在不同分支中返回函數前的 "Close()" 語句。
func handleConnections(w http.ResponseWriter, r *http.Request) { // Upgrade initial GET request to a websocket ws, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Fatal(err) } // Make sure we close the connection when the function returns defer ws.Close()
接下來把新的用戶端添加到全域的 "clients" 映射表中進行註冊,這個映射表在早先已經建立了。
func handleConnections(w http.ResponseWriter, r *http.Request) { ... // Register our new client clients[ws] = true
最後一步是一個無限迴圈,它一直等待著要寫入 WebSocket 的新訊息,將其從 JSON 還原序列化為 Message 對象然後送入廣播頻道。然而 "handleMessages()" Go 程式就能把它送給串連中的其它用戶端。
邊城 翻譯於 5個月前 2人頂 頂 翻譯得不錯哦! 如果從 socket 中讀取資料有誤,我們假設用戶端已經因為某種原因斷開。我們記錄錯誤並從全域的 “clients” 映射表裡刪除該用戶端,這樣一來,我們不會繼續嘗試與其通訊。
另外,HTTP 路由處理函數已經被作為 goroutines 運行。這使得 HTTP 伺服器無需等待另一個串連完成,就能處理多個傳入串連。
func handleConnections(w http.ResponseWriter, r *http.Request) { ... for { var msg Message // Read in a new message as JSON and map it to a Message object err := ws.ReadJSON(&msg) if err != nil { log.Printf("error: %v", err) delete(clients, ws) break } // Send the newly received message to the broadcast channel broadcast <- msg }} 伺服器的最後一部分是"handleMessages()"函數。這是一個簡單迴圈,從“broadcast”中連續讀取資料,然後通過各自的 WebSocket 串連將訊息傳播到所以用戶端。同樣,如果寫入 Websocket 時出現錯誤,我們將關閉串連,並將其從“clients” 映射中刪除。
func handleMessages() { for { // Grab the next message from the broadcast channel msg := <-broadcast // Send it out to every client that is currently connected for client := range clients { err := client.WriteJSON(msg) if err != nil { log.Printf("error: %v", err) client.Close() delete(clients, client) } } }} 達爾文 翻譯於 5個月前 1人頂 頂 翻譯得不錯哦!