本文同步至 http://www.waylau.com/go-websocket-chat/javascript
這個示例應用程序展現瞭如何使用 WebSocket, Golang 和 jQuery 建立一個簡單的web聊天應用程序。這個示例的源代碼在 https://github.com/waylau/goChat 。css
##Running the example 運行示例html
這個示例須要 Golang 開發環境。 該頁面描述如何安裝開發環境。java
一旦你去啓動和運行,您能夠下載、構建和運行的例子, 使用命令:jquery
go get gary.burd.info/go-websocket-chat go-websocket-chat
在支持 websocket 的瀏覽器嘗試打開 http://127.0.0.1:8080/ 啓動應用git
##Server 服務器github
服務器程序實現了 http 包,包含了 Go 分發和 Gorilla 項目的 websocket 包.golang
應用程序定義了兩種類型, connection 和 hub 。服務器爲每一個 webscocket 鏈接 建立的一個 connection 類型的實例 。 鏈接器扮演了 websocket 和 hub 類型單例 之間的媒介 。 hub 保持一組註冊了的鏈接器 和 廣播到鏈接器的信息。web
程序運行了一個 goroutine 給 hub 和兩個 goroutine 給每一個鏈接器。 goroutine 經過 channel 和其餘進行交流。 hub 擁有註冊鏈接器、註銷鏈接器和廣播信息的 channel。一個連機器擁有緩存的發出信息的 channel 。其中一個 鏈接器的 goroutine 從這個 channel 中讀信息 並把信息寫入 webscoket。另一個鏈接器 goroutine 從 websocket 讀信息,並把信息發送到 hub。ajax
下面是 hub 類型代碼:
package main type hub struct { // 註冊了的鏈接器 connections map[*connection]bool // 從鏈接器中發入的信息 broadcast chan []byte // 從鏈接器中註冊請求 register chan *connection // 從鏈接器中註銷請求 unregister chan *connection } var h = hub{ broadcast: make(chan []byte), register: make(chan *connection), unregister: make(chan *connection), connections: make(map[*connection]bool), } func (h *hub) run() { for { select { case c := <-h.register: h.connections[c] = true case c := <-h.unregister: if _, ok := h.connections[c]; ok { delete(h.connections, c) close(c.send) } case m := <-h.broadcast: for c := range h.connections { select { case c.send <- m: default: delete(h.connections, c) close(c.send) } } } } }
應用程序的 主要 函數啓動 hub 以 goroutine 形式運行方法。鏈接器 發送請求到 hub 經過 註冊、註銷和廣播 channel。
hub 註冊鏈接器經過添加 connection 的指針做爲 connections map 的主鍵。這個 map 的值一般是 true。
註銷的代碼有點複雜。除了從 connections map 刪除鏈接器的指針外, hub 關閉了 connection 的發送,來標識沒有信息再被髮送到 connection了。
hub 經過循環註冊鏈接器和發送信息到鏈接器的發送 channel 來控制信息。 若是鏈接器的發送緩衝區已經滿了,那麼 hub 假設 客戶端已死或卡住了。這種狀況下, hub 註銷鏈接器 並關閉 websocket.
下面關於 connection 類型的代碼:
package main import ( "github.com/gorilla/websocket" "net/http" ) type connection struct { // websocket 鏈接器 ws *websocket.Conn // 發送信息的緩衝 channel send chan []byte } func (c *connection) reader() { for { _, message, err := c.ws.ReadMessage() if err != nil { break } h.broadcast <- message } c.ws.Close() } func (c *connection) writer() { for message := range c.send { err := c.ws.WriteMessage(websocket.TextMessage, message) if err != nil { break } } c.ws.Close() } var upgrader = &websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024} func wsHandler(w http.ResponseWriter, r *http.Request) { ws, err := upgrader.Upgrade(w, r, nil) if err != nil { return } c := &connection{send: make(chan []byte, 256), ws: ws} h.register <- c defer func() { h.unregister <- c }() go c.writer() c.reader() }
wsHandler 方法被主函數當作http handler註冊。HTTP 鏈接到 WebSocket 協議的升級,建立一個鏈接對象,註冊這個鏈接到 sub ,並經過 defer延遲語句 來控制 鏈接的註銷。
接着,wsHandler 方法開啓 鏈接器的寫入方法做爲一個 goroutine。 寫入方法將信息從鏈接器的 channel 轉入 websocket。當 hub 關閉 channel 或者 在寫入 websocket 時出錯,寫入方法關閉。
最後,wsHandler 方法 調用鏈接器的 讀 方法。 讀方法將 入站消息 從 websocket 轉到 hub。
這裏是服務器的代碼的其他部分:
package main import ( "flag" "go/build" "log" "net/http" "path/filepath" "text/template" ) var ( addr = flag.String("addr", ":8080", "http service address") assets = flag.String("assets", defaultAssetPath(), "path to assets") homeTempl *template.Template ) func defaultAssetPath() string { p, err := build.Default.Import("gary.burd.info/go-websocket-chat", "", build.FindOnly) if err != nil { return "." } return p.Dir } func homeHandler(c http.ResponseWriter, req *http.Request) { homeTempl.Execute(c, req.Host) } func main() { flag.Parse() homeTempl = template.Must(template.ParseFiles(filepath.Join(*assets, "home.html"))) go h.run() http.HandleFunc("/", homeHandler) http.HandleFunc("/ws", wsHandler) if err := http.ListenAndServe(*addr, nil); err != nil { log.Fatal("ListenAndServe:", err) } }
應用主程序啓動 hub goroutine。 接着 主程序 註冊 主頁 和 websocket 鏈接器的控制器N。最後主程序啓動 HTTP 服務器。
##Client 客戶端
客戶端的實現是一個簡單的 HTML 文件:
<html> <head> <title>Chat Example</title> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> <script type="text/javascript"> $(function() { var conn; var msg = $("#msg"); var log = $("#log"); function appendLog(msg) { var d = log[0] var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight; msg.appendTo(log) if (doScroll) { d.scrollTop = d.scrollHeight - d.clientHeight; } } $("#form").submit(function() { if (!conn) { return false; } if (!msg.val()) { return false; } conn.send(msg.val()); msg.val(""); return false }); if (window["WebSocket"]) { conn = new WebSocket("ws://{{$}}/ws"); conn.onclose = function(evt) { appendLog($("<div><b>Connection closed.</b></div>")) } conn.onmessage = function(evt) { appendLog($("<div/>").text(evt.data)) } } else { appendLog($("<div><b>Your browser does not support WebSockets.</b></div>")) } }); </script> <style type="text/css"> html { overflow: hidden; } body { overflow: hidden; padding: 0; margin: 0; width: 100%; height: 100%; background: gray; } #log { background: white; margin: 0; padding: 0.5em 0.5em 0.5em 0.5em; position: absolute; top: 0.5em; left: 0.5em; right: 0.5em; bottom: 3em; overflow: auto; } #form { padding: 0 0.5em 0 0.5em; margin: 0; position: absolute; bottom: 1em; left: 0px; width: 100%; overflow: hidden; } </style> </head> <body> <div id="log"></div> <form id="form"> <input type="submit" value="Send" /> <input type="text" id="msg" size="64"/> </form> </body> </html>
客戶端使用 jQuery
文檔加載。腳本檢查 websocket 的功能 。若是 WebSocket 功能 能夠用,而後打開腳本與服務器的鏈接,並註冊一個回調處理來自服務器的信息。回調使用 appendlog 方法將消息添加到聊天記錄。
appendlog 方法檢查在添加新的內容時的滾動位置,從而可讓用戶手動滾動聊天記錄而不會被新來的消息中斷。若是聊天記錄滾動至底部,那麼新內容添加的到舊內容的後面。不然,滾動的位置不會改變。
表單處理器將用戶的輸入寫入到 WebSocket 而且清除輸入字段。