Building a real-time whiteboard using Golang

Source: Internet
Author: User
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

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

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.