https://github.com/chapin666/simple-drawing-backendhtml
首先,咱們須要建立一個用於與用戶交互消息的橋樑(Hub)。這個思路相似於Gorilla's 的 chat 例子。前端
建立一個 client.go 文件git
package main import ( "github.com/gorilla/websocket" uuid "github.com/satori/go.uuid" ) type Client struct { id string hub *Hub color string socket *websocket.Conn outbound chan []byte }
爲client編寫一個構造方法,這裏使用了UUID和隨機顏色庫github
func newClient(hub *Hub, socket *websocket.Conn) *Client { return &Client{ id: uuid.NewV4().String(), color: generateColor(), hub: hub, socket: socket, outbound: make(chan []byte), } }
新建 utilities.go 文件來編程 generateColor 方法web
package main import ( "math/rand" "time" colorful "github.com/lucasb-eyer/go-colorful" ) func init() { rand.Seed(time.Now().UnixNano()) } func generateColor() string { c := colorful.Hsv(rand.Float64()*360.0, 0.8, 0.8) return c.Hex() }
寫一個用於到Hub讀取消息 read 的方法,若是有錯誤發生,將會通知unregistered通道編程
func (client *Client) read() { defer func() { client.hub.unregister <- client }() for { _, data, err := client.socket.ReadMessage() if err != nil { break } client.hub.onMessage(data, client) } }
write方法從outbound通道獲取消息併發送給用戶。這樣,服務器將可以發送消息到客戶端。json
func (client *Client) write() { for { select { case data, ok := <-client.outbound: if !ok { client.socket.WriteMessage(websocket.CloseMessage, []byte{}) return } client.socket.WriteMessage(websocket.TextMessage, data) } } }
在 client struct中添加 啓動 和 結束 進程的方法,而且在啓動方法中使用 goroutine 運行 read 和 write 方法canvas
func (client Client) run() { go client.read() go client.write() } func (client Client) close() { client.socket.Close() close(client.outbound) }
新建 hub.go 文件並聲明 Hub structapi
package main import ( "encoding/json" "log" "net/http" "github.com/gorilla/websocket" "github.com/tidwall/gjson" ) type Hub struct { clients []*Client register chan *Client unregister chan *Client }
添加構造方法數組
func newHub() *Hub { return &Hub{ clients: make([]*Client, 0), register: make(chan *Client), unregister: make(chan *Client), } }
添加 run 方法
func (hub *Hub) run() { for { select { case client := <-hub.register: hub.onConnect(client) case client := <-hub.unregister: hub.onDisconnect(client) } } }
編寫一個將http升級到WebSockets請求的方法。 若是升級成功,客戶端將被添加到 clients 中。
var upgrader = websocket.Upgrader{ // Allow all origins CheckOrigin: func(r *http.Request) bool { return true }, } func (hub *Hub) handleWebSocket(w http.ResponseWriter, r *http.Request) { socket, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println(err) http.Error(w, "could not upgrade", http.StatusInternalServerError) return } client := newClient(hub, socket) hub.clients = append(hub.clients, client) hub.register <- client client.run() }
編寫一個發送消息到客戶端的方法
func (hub *Hub) send(message interface{}, client *Client) { data, _ := json.Marshal(message) client.outbound <- data }
編寫一個廣播(broadcast)消息到全部客戶端的方法(排除本身)
func (hub *Hub) broadcast(message interface{}, ignore *Client) { data, _ := json.Marshal(message) for _, c := range hub.clients { if c != ignore { c.outbound <- data } } }
Messages 將使用JSON做爲交互格式。 每條消息將攜帶一個「kind」字段,以區分消息
新建messages.go文件並建立 message 包
聲明全部消息「kinds」的枚舉
package message const ( // KindConnected is sent when user connects KindConnected = iota + 1 // KindUserJoined is sent when someone else joins KindUserJoined // KindUserLeft is sent when someone leaves KindUserLeft // KindStroke message specifies a drawn stroke by a user KindStroke // KindClear message is sent when a user clears the screen KindClear )
聲明一些簡單的數據結構
type Point struct { X int `json:"x"` Y int `json:"y"` } type User struct { ID string `json:"id"` Color string `json:"color"` }
聲明全部的消息類型結構體並編寫 構造函數。kind 字段在構造函數中設置
type Connected struct { Kind int `json:"kind"` Color string `json:"color"` Users []User `json:"users"` } func NewConnected(color string, users []User) *Connected { return &Connected{ Kind: KindConnected, Color: color, Users: users, } } type UserJoined struct { Kind int `json:"kind"` User User `json:"user"` } func NewUserJoined(userID string, color string) *UserJoined { return &UserJoined{ Kind: KindUserJoined, User: User{ID: userID, Color: color}, } } type UserLeft struct { Kind int `json:"kind"` UserID string `json:"userId"` } func NewUserLeft(userID string) *UserLeft { return &UserLeft{ Kind: KindUserLeft, UserID: userID, } } type Stroke struct { Kind int `json:"kind"` UserID string `json:"userId"` Points []Point `json:"points"` Finish bool `json:"finish"` } type Clear struct { Kind int `json:"kind"` UserID string `json:"userId"` }
返回hub.go文件,添加全部缺乏的功能。
onConnect函數表示客戶端鏈接,在 run方法中調用。 它將用戶的畫筆顏色和其餘用戶的信息發送給客戶端。 它還將當前鏈接的用戶信息通知給其餘在線用戶。
func (hub *Hub) onConnect(client *Client) { log.Println("client connected: ", client.socket.RemoteAddr()) // Make list of all users users := []message.User{} for _, c := range hub.clients { users = append(users, message.User{ID: c.id, Color: c.color}) } // Notify user joined hub.send(message.NewConnected(client.color, users), client) hub.broadcast(message.NewUserJoined(client.id, client.color), client) }
onDisconnect函數從 clients 中刪除斷開鏈接的客戶端,並通知其餘人有人離開。
func (hub *Hub) onDisconnect(client *Client) { log.Println("client disconnected: ", client.socket.RemoteAddr()) client.close() // Find index of client i := -1 for j, c := range hub.clients { if c.id == client.id { i = j break } } // Delete client from list copy(hub.clients[i:], hub.clients[i+1:]) hub.clients[len(hub.clients)-1] = nil hub.clients = hub.clients[:len(hub.clients)-1] // Notify user left hub.broadcast(message.NewUserLeft(client.id), nil) }
每當從客戶端收到消息時,都會調用onMessage函數。 首先經過使用tidwall/gjson包來讀取它是什麼樣的消息,而後分別處理每一個狀況。
在這個例子中,狀況都是類似的。 每一個消息得到用戶的ID,而後轉發給其餘客戶端。
func (hub *Hub) onMessage(data []byte, client *Client) { kind := gjson.GetBytes(data, "kind").Int() if kind == message.KindStroke { var msg message.Stroke if json.Unmarshal(data, &msg) != nil { return } msg.UserID = client.id hub.broadcast(msg, client) } else if kind == message.KindClear { var msg message.Clear if json.Unmarshal(data, &msg) != nil { return } msg.UserID = client.id hub.broadcast(msg, client) } }
最後,編寫main.go文件
package main import ( "log" "net/http" ) func main() { hub := newHub() go hub.run() http.HandleFunc("/ws", hub.handleWebSocket) err := http.ListenAndServe(":3000", nil) if err != nil { log.Fatal(err) } }
前端應用程序將用純JavaScript編寫。 在client目錄建立index.html文件
<!DOCTYPE html> <html> <head> <title>Collaborative Drawing App</title> <style> #canvas { border: 1px solid #000; } </style> </head> <body> <canvas id="canvas" width="480" height="360"> </canvas> <div> <button id="clearButton">Clear</button> </div> <script> MESSAGE_CONNECTED = 1; MESSAGE_USER_JOINED = 2; MESSAGE_USER_LEFT = 3; MESSAGE_STROKE = 4; MESSAGE_CLEAR = 5; window.onload = function () {} </script> </body> </html>
上面的代碼會建立一個畫布和一個清除按鈕。 如下全部JavaScript代碼都編寫在window.onload事件處理程序中。
聲明一些變量
var canvas = document.getElementById('canvas'); var ctx = canvas.getContext("2d"); var isDrawing = false; var strokeColor = ''; var strokes = [];
編寫canvas處理事件
canvas.onmousedown = function (event) { isDrawing = true; addPoint(event.pageX - this.offsetLeft, event.pageY - this.offsetTop, true); }; canvas.onmousemove = function (event) { if (isDrawing) { addPoint(event.pageX - this.offsetLeft, event.pageY - this.offsetTop); } }; canvas.onmouseup = function () { isDrawing = false; }; canvas.onmouseleave = function () { isDrawing = false; };
編寫addPoint方法,strokes是一個畫筆數組,存儲全部的點。
function addPoint(x, y, newStroke) { var p = { x: x, y: y }; if (newStroke) { strokes.push([p]); } else { strokes[strokes.length - 1].push(p); } update(); }
update 方法重繪
function update() { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.lineJoin = 'round'; ctx.lineWidth = 4; ctx.strokeStyle = strokeColor; drawStrokes(strokes); }
drawStrokes 繪製多個路徑
function drawStrokes(strokes) { for (var i = 0; i < strokes.length; i++) { ctx.beginPath(); for (var j = 1; j < strokes[i].length; j++) { var prev = strokes[i][j - 1]; var current = strokes[i][j]; ctx.moveTo(prev.x, prev.y); ctx.lineTo(current.x, current.y); } ctx.closePath(); ctx.stroke(); } }
清除 點擊事件
document.getElementById('clearButton').onclick = function () { strokes = []; update(); };
要與服務器通訊,請首先聲明一些額外的變量。
var socket = new WebSocket("ws://localhost:3000/ws"); var otherColors = {}; var otherStrokes = {};
otherColors對象將保存其餘客戶端的顏色,其中key將是用戶ID。 otherStrokes將保存繪圖數據。
在addPoint函數中增長髮送消息。 對於這個例子,points數組只有一個點。 理想狀況下,分數將根據一些標準分批發送。
function addPoint(x, y, newStroke) { var p = { x: x, y: y }; if (newStroke) { strokes.push([p]); } else { strokes[strokes.length - 1].push(p); } socket.send(JSON.stringify({ kind: MESSAGE_STROKE, points: [p], finish: newStroke })); update(); }
處理髮送 "clear" 消息
document.getElementById('clearButton').onclick = function () { strokes = []; socket.send(JSON.stringify({ kind: MESSAGE_CLEAR })); update(); };
onmessage 處理函數
socket.onmessage = function (event) { var messages = event.data.split('\n'); for (var i = 0; i < messages.length; i++) { var message = JSON.parse(messages[i]); onMessage(message); } }; function onMessage(message) { switch (message.kind) { case MESSAGE_CONNECTED: break; case MESSAGE_USER_JOINED: break; case MESSAGE_USER_LEFT: break; case MESSAGE_STROKE: break; case MESSAGE_CLEAR: break; } }
對於 MESSAGE_CONNECTED 狀況,設置用戶的畫筆顏色並用給定的信息填充「other」對象。
strokeColor = message.color; for (var i = 0; i < message.users.length; i++) { var user = message.users[i]; otherColors[user.id] = user.color; otherStrokes[user.id] = []; }
對於MESSAGE_USER_JOINED的狀況,設置用戶的顏色並準備一個空的筆劃數組。
otherColors[message.user.id] = message.user.color; otherStrokes[message.user.id] = [];
在MESSAGE_USER_LEFT的狀況下,若是有人離開,須要刪除他的數據,並從畫布上清除他的繪畫。
delete otherColors[message.userId]; delete otherStrokes[message.userId]; update();
在MESSAGE_STROKE的狀況下,更新用戶的筆畫數組。
if (message.finish) { otherStrokes[message.userId].push(message.points); } else { var strokes = otherStrokes[message.userId]; strokes[strokes.length - 1] = strokes[strokes.length - 1].concat(message.points); } update();
對於MESSAGE_CLEAR狀況,只需清除用戶的筆劃數組。
otherStrokes[message.userId] = []; update();
更新update方法以顯示他人的圖紙。
function update() { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.lineJoin = 'round'; ctx.lineWidth = 4; // Draw mine ctx.strokeStyle = strokeColor; drawStrokes(strokes); // Draw others' var userIds = Object.keys(otherColors); for (var i = 0; i < userIds.length; i++) { var userId = userIds[i]; ctx.strokeStyle = otherColors[userId]; drawStrokes(otherStrokes[userId]); } }