The complete code for this section: GitHub

This article is the fourth in a series on building chat applications using ReactJS and Go. You can find part 3 – front-end implementation here

This section mainly implements the function of processing multiple client messages and broadcasting the received messages to each connected client. By the end of this part of the series, we will:

  • A pooling mechanism is implemented to effectively track the number of connections in WebSocket services.
  • The ability to broadcast any received message to all connections in the connection pool.
  • The ability to notify existing clients when another client connects or disconnects.

At the end of this part of the course, our application looks like this:

Split the Websocket code

Now that the necessary basic work is done, we can move on to improving the code base. Some applications can be split into subpackages for easy development.

Now, ideally, your main.go file should be just an entry point to the Go application, it should be fairly small, and it should be able to call other packages in your project.

Note – we will refer to the unofficial Go project structure layout – Golang-standards/project-Layout

Let’s create a new directory called PKG/in the back-end project directory. In the meantime, we’ll create another directory called websocket/ that will contain the websocket.go file.

We’ll move a lot of the Websocket-based code we currently use in the main.go file into this new websocket.go file.

Note – One thing to note is that when copying functions, we need to capitalize the first letter of each function, which we want to be exportable for the rest of the project.

package websocket

import (
    "fmt"
    "io"
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool { return true}},func Upgrade(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
    ws, err := upgrader.Upgrade(w, r, nil)
    iferr ! =nil {
        log.Println(err)
        return ws, err
    }
    return ws, nil
}

func Reader(conn *websocket.Conn) {
    for {
        messageType, p, err := conn.ReadMessage()
        iferr ! =nil {
            log.Println(err)
            return
        }

        fmt.Println(string(p))

        iferr := conn.WriteMessage(messageType, p); err ! =nil {
            log.Println(err)
            return}}}func Writer(conn *websocket.Conn) {
    for {
        fmt.Println("Sending")
        messageType, r, err := conn.NextReader()
        iferr ! =nil {
            fmt.Println(err)
            return
        }
        w, err := conn.NextWriter(messageType)
        iferr ! =nil {
            fmt.Println(err)
            return
        }
        if_, err := io.Copy(w, r); err ! =nil {
            fmt.Println(err)
            return
        }
        iferr := w.Close(); err ! =nil {
            fmt.Println(err)
            return}}}Copy the code

Now that we’ve created the new WebSocket package, we want to update the main.go file to invoke the package. You must first add a new import to the import list at the top of the file, and you can do this by using webSocket. To call the functions in the package. Like this:

package main

import (
    "fmt"
    "net/http"

    "realtime-chat-go-react/backend/pkg/websocket"
)

func serveWs(pool *websocket.Pool, w http.ResponseWriter, r *http.Request) {
    fmt.Println("WebSocket Endpoint Hit")
    conn, err := websocket.Upgrade(w, r)
    iferr ! =nil {
        fmt.Fprintf(w, "%+v\n", err)
    }

    client := &websocket.Client{
        Conn: conn,
        Pool: pool,
    }

    pool.Register <- client
    client.Read()
}

func setupRoutes(a) {
    pool := websocket.NewPool()
    go pool.Start()

    http.HandleFunc("/ws".func(w http.ResponseWriter, r *http.Request) {
        serveWs(pool, w, r)
    })
}

func main(a) {
    fmt.Println("Distributed Chat App v0.01")
    setupRoutes()
    http.ListenAndServe(": 8080".nil)}Copy the code

After these changes, we should check to see if these break existing functionality. Try running the back end and front end again to ensure that messages can still be sent and received:

$ cd backend/
$ go run main.go
Copy the code

If successful, we can continue to extend the code base to handle multiple clients.

By now, the directory structure should look like this:

- backend/
- - pkg/
- - - websocket/
- - - - websocket.go
- - main.go
- - go.mod
- - go.sum
- frontend/
- ...
Copy the code

Handling multiple clients

Now that the basics are out of the way, we can move on to improving the back end and implementing the ability to handle multiple clients.

To do this, we need to consider how to handle the connection to the WebSocket service. Each time a new connection is established, we must add them to the existing connection pool and ensure that every time a message is sent, everyone in the pool receives the message.

The use of Channels

We need to develop a system with a large number of concurrent connections. A new Goroutine is started to handle each connection for the duration of the connection. This means that we must care about communication between these concurrent Goroutines and ensure thread-safety.

When further implementing the Pool structure, we must consider using sync.Mutex to block other Goroutines while accessing/modifying data, or we could use channels.

For this project, I think it’s best to use channels and communicate in a safe way across multiple concurrent Goroutines.

Note – If you want to learn more about Channels in Go, check out my other article, The Go Channels Tutorial, here

client.go

Let’s start by creating a new file called client.go, which will reside in the PKG /websocket directory and will define a client structure containing the following:

  • ID: a uniquely identifiable string for a particular connection
  • Conn: point towebsocket.ConnA pointer to the
  • Pool: point toPoolA pointer to the

You also need to define a Read() method that will always listen for new messages on this Client’s WebSocket connection.

If it receives new messages, it passes them to the pool’s Broadcast channel, which then broadcasts the received messages to each client in the pool.

package websocket

import (
    "fmt"
    "log"

    "github.com/gorilla/websocket"
)

type Client struct {
    ID   string
    Conn *websocket.Conn
    Pool *Pool
}

type Message struct {
    Type int    `json:"type"`
    Body string `json:"body"`
}

func (c *Client) Read(a) {
    defer func(a) {
        c.Pool.Unregister <- c
        c.Conn.Close()
    }()

    for {
        messageType, p, err := c.Conn.ReadMessage()
        iferr ! =nil {
            log.Println(err)
            return
        }
        message := Message{Type: messageType, Body: string(p)}
        c.Pool.Broadcast <- message
        fmt.Printf("Message Received: %+v\n", message)
    }
}
Copy the code

Great, now that we’ve defined the client in our code, let’s go ahead and implement the pool.

The Pool structure

We create a new file pool.go in the PKG /websocket directory.

We’ll start by defining a Pool structure that will contain all the channels we need for concurrent communication, as well as a client map.

package websocket

import "fmt"

type Pool struct {
    Register   chan *Client
    Unregister chan *Client
    Clients    map[*Client]bool
    Broadcast  chan Message
}

func NewPool(a) *Pool {
    return &Pool{
        Register:   make(chan *Client),
        Unregister: make(chan *Client),
        Clients:    make(map[*Client]bool),
        Broadcast:  make(chan Message),
    }
}
Copy the code

We need to ensure that only one point in the application can write to WebSocket connections, or we will face concurrent write problems. So, we define the Start() method, which will always listen for content passed to Pool Channels, and then take action if it receives content sent to one of the channels.

  • Register– When a new client connects,Register channelIs sent to all clients in this poolNew User Joined...
  • Unregister – Unregister the user to notify the pool when the client disconnects
  • Clients – Boolean mapping of the client. You can use Boolean values to determine client activity/inactivity
  • Broadcast-a channel that, when delivering a message, traverses all clients in the pool and sends the message over the socket.

Code:

func (pool *Pool) Start(a) {
    for {
        select {
        case client := <-pool.Register:
            pool.Clients[client] = true
            fmt.Println("Size of Connection Pool: ".len(pool.Clients))
            for client, _ := range pool.Clients {
                fmt.Println(client)
                client.Conn.WriteJSON(Message{Type: 1, Body: "New User Joined..."})}break
        case client := <-pool.Unregister:
            delete(pool.Clients, client)
            fmt.Println("Size of Connection Pool: ".len(pool.Clients))
            for client, _ := range pool.Clients {
                client.Conn.WriteJSON(Message{Type: 1, Body: "User Disconnected..."})}break
        case message := <-pool.Broadcast:
            fmt.Println("Sending message to all clients in Pool")
            for client, _ := range pool.Clients {
                iferr := client.Conn.WriteJSON(message); err ! =nil {
                    fmt.Println(err)
                    return
                }
            }
        }
    }
}
Copy the code

websocket.go

Great, let’s make a few more minor changes to the websocket.go file and remove some functions and methods that are no longer needed:

package websocket

import (
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool { return true}},func Upgrade(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
    conn, err := upgrader.Upgrade(w, r, nil)
    iferr ! =nil {
        log.Println(err)
        return nil, err
    }

    return conn, nil
}
Copy the code

Update the main. Go

Finally, we need to update the main.go file, create a new Client on each connection, and register the Client with the Pool:

package main

import (
    "fmt"
    "net/http"

    "github.com/TutorialEdge/realtime-chat-go-react/pkg/websocket"
)

func serveWs(pool *websocket.Pool, w http.ResponseWriter, r *http.Request) {
    fmt.Println("WebSocket Endpoint Hit")
    conn, err := websocket.Upgrade(w, r)
    iferr ! =nil {
        fmt.Fprintf(w, "%+v\n", err)
    }

    client := &websocket.Client{
        Conn: conn,
        Pool: pool,
    }

    pool.Register <- client
    client.Read()
}

func setupRoutes(a) {
    pool := websocket.NewPool()
    go pool.Start()

    http.HandleFunc("/ws".func(w http.ResponseWriter, r *http.Request) {
        serveWs(pool, w, r)
    })
}

func main(a) {
    fmt.Println("Distributed Chat App v0.01")
    setupRoutes()
    http.ListenAndServe(": 8080".nil)}Copy the code

test

Now that we’ve made all the necessary changes, we should test what we’ve done and make sure everything works as expected.

Start your back-end application:

$ go run main.go
Distributed Chat App v0.01
Copy the code

If you open http://localhost:3000 in several browsers, you can see that they automatically connect to the back-end WebSocket service, and now we can send and receive messages from other clients in the same pool!

conclusion

In this section, we managed to implement a way to process multiple clients and broadcast messages to everyone connected in the connection pool.

Now it’s getting interesting. We can add new features, such as custom messages, in the next section.

Next section: Part 5 – Optimizing the front end


Original text: tutorialedge.net/projects/ch…

Author: Elliot Forbes

This article is originally compiled by GCTT and published by Go Chinese