This is a creation in Article, where the information may have evolved or changed.
First, we need to create a bridge (Hub) for interacting with the user message. This idea is similar to the chat example of Gorilla ' s.
Client struct
Create a Client.go file
package mainimport ( "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}
Write a construction method for the client, where UUID and random color libraries are used
func newClient(hub *Hub, socket *websocket.Conn) *Client { return &Client{ id: uuid.NewV4().String(), color: generateColor(), hub: hub, socket: socket, outbound: make(chan []byte), }}
Create a new Utilities.go file to program the Generatecolor method
package mainimport ( "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()}
Write a method to read the message to the hub, and if any errors occur, the unregistered channel will be notified
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) }}
The Write method obtains the message from the outbound channel and sends it to the user. In this way, the server will be able to send messages to the client.
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) } }}
Add methods to start and end processes in the client struct, and use Goroutine to run the read and write methods in the Startup method
func (client Client) run() { go client.read() go client.write()}func (client Client) close() { client.socket.Close() close(client.outbound)}
Hub struct
Create a new Hub.go file and declare the hub struct
package mainimport ( "encoding/json" "log" "net/http" "github.com/gorilla/websocket" "github.com/tidwall/gjson")type Hub struct { clients []*Client register chan *Client unregister chan *Client}
Adding a construction method
func newHub() *Hub { return &Hub{ clients: make([]*Client, 0), register: make(chan *Client), unregister: make(chan *Client), }}
Add the Run method
func (hub *Hub) run() { for { select { case client := <-hub.register: hub.onConnect(client) case client := <-hub.unregister: hub.onDisconnect(client) } }}
Write a method that upgrades HTTP to the WebSockets request. If the upgrade succeeds, the client will be added to the 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()}
Write a method to send a message to the client
func (hub *Hub) send(message interface{}, client *Client) { data, _ := json.Marshal(message) client.outbound <- data}
Method of writing a broadcast (broadcast) message to all clients (excluding yourself)
func (hub *Hub) broadcast(message interface{}, ignore *Client) { data, _ := json.Marshal(message) for _, c := range hub.clients { if c != ignore { c.outbound <- data } }}
Messages
Messages will use JSON as an interactive format. Each message will carry a "kind" field to differentiate the message
New Messages.go file and create message package
Enumeration that declares all messages "kinds"
package messageconst ( // 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)
Declare a few simple data structures
type Point struct { X int `json:"x"` Y int `json:"y"`}type User struct { ID string `json:"id"` Color string `json:"color"`}
Declares all message-type structs and writes constructors. The kind field is set in the constructor
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"`}
Handling Message Flow
Return to the Hub.go file and add any missing features.
The OnConnect function represents a client connection that is called in the Run method. It sends the user's brush color and other user's information to the client. It also notifies other online users of the current connection's user information.
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)}
The OnDisconnect function removes the disconnected client from the clients and notifies others that someone has left.
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)}
The OnMessage function is called whenever a message is received from the client. Start by using the Tidwall/gjson package to read what the message is, and then handle each case separately.
In this case, the situation is similar. Each message obtains the user's ID and is then forwarded to the other client.
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) }}
Finally, write the Main.go file
package mainimport ( "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) }}
Front-End app
The front-end application will be written in pure JavaScript. Create a index.html file in the client directory
<!DOCTYPE html>
The code above creates a canvas and a clear button. All of the following JavaScript code is written in the Window.onload event handler.
Drawing on canvas
Declare some variables
var canvas = document.getElementById('canvas');var ctx = canvas.getContext("2d");var isDrawing = false;var strokeColor = '';var strokes = [];
Writing a canvas handling event
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;};
Writing the Addpoint method, strokes is a brush array that stores all the points.
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 method Redraw
function update() { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.lineJoin = 'round'; ctx.lineWidth = 4; ctx.strokeStyle = strokeColor; drawStrokes(strokes);}
Drawstrokes Drawing multiple paths
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(); }}
Clear Click events
document.getElementById('clearButton').onclick = function () { strokes = []; update();};
Server Communication
To communicate with the server, first declare some additional variables.
var socket = new WebSocket("ws://localhost:3000/ws");var otherColors = {};var otherStrokes = {};
The Othercolors object will hold the color of the other client, where key will be the user ID. Otherstrokes will save the drawing data.
Increase the sending message in the Addpoint function. For this example, the points array has only one point. Ideally, the scores will be sent in batches according to some criteria.
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();}
Process send "clear" message
document.getElementById('clearButton').onclick = function () { strokes = []; socket.send(JSON.stringify({ kind: MESSAGE_CLEAR })); update();};
OnMessage processing function
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; }}
For message_connected cases, set the user's brush color and populate the "other" object with the given information.
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] = [];}
For message_user_joined, set the user's color and prepare an empty array of strokes.
otherColors[message.user.id] = message.user.color;otherStrokes[message.user.id] = [];
In the case of message_user_left, if someone leaves, need to delete his data and remove his painting from the canvas.
delete otherColors[message.userId];delete otherStrokes[message.userId];update();
In the case of Message_stroke, update the user's stroke array.
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();
For message_clear cases, simply clear the user's stroke array.
otherStrokes[message.userId] = [];update();
Updates the Update method to display other people's drawings.
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]); }}
Source
Https://github.com/chapin666/simple-drawing-backend