This blog is the first part of a multi part series.
- . It also creates a volume that maps a file
nginx.confto the file/etc/nginx/nginx.confinside the nginx container.
Let’s create the
nginx.conffile now.
CODEevents {
}
http {
upstream go_chat {
server app:8080;
}
server {
listen 80;
location / {
proxy_pass http://go_chat;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header Upgrade websocket;
proxy_set_header Connection Upgrade;
}
}
}
This configuration makes sure that any requests reaching localhost:80 are proxied to our web instances. It also sets some headers that are required for websockets to work as expected.
Also, we need to update the url used to create the websocket connection in
index.htmlfromlocalhost:8080/wstolocalhost:80/wssince all requests will go through the nginx server.
Time for some experimentation!! Let’s run the docker containers.
CODEdocker-compose build
docker-compose up -d
Now, let’s test our chat app by opening
localhostin two browser windows side by side.
From the logs, we can see that both the browsers' websocket connections are handled by different server instances and so messages broadcasted by
go_chat-app-1instance do not reach the clients connected togo_chat-app-2instance, as each server only broadcasts to the connections handled by them.
Now, to solve this problem, we will be using the Pub/Sub feature of redis. Other than being a database, redis can also act like a message broker and we will be leveraging that in our web application. Lets install the redis client library written in go using go get
CODEgo get github.com/redis/go-redis/v9
Let’s create a redis client in the
mainfunction and pass it to theserveWsfunction.
CODEfunc main() {
redisHost := os.Getenv("REDIS_HOST")
redisPort := os.Getenv("REDIS_PORT")
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", redisHost, redisPort),
})
// Rest of the main function
}
Let’s rewrite the
serveWsfunction like this
CODEfunc serveWs(rdb *redis.Client) func(c *gin.Context) {
return func(c *gin.Context) {
upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("Error in upgrading web socket. Error: %v", err)
return
}
go handleClient(conn, rdb)
}
}
Let’s see what has changed
serveWsis now a function that accepts a redis client and returns a gin handler.- The redis client is passed to the
handleClientfunction.
Now let's modify the
handleClientfunction
CODEconst channel = "chat"
func handleClient(c *websocket.Conn, rdb *redis.Client) {
defer func() {
delete(clients, c)
log.Println("Closing Websocket")
c.Close()
}()
clients[c] = struct{}{}
for {
var msg Message
err := c.ReadJSON(&msg)
if err != nil {
log.Printf("Error in reading json message. Error : %v", err)
return
}
msgBytes, err := json.Marshal(msg)
if err != nil {
fmt.Println("Err marshaling", err.Error())
return
}
err = rdb.Publish(context.Background(), channel, string(msgBytes)).Err()
if err != nil {
fmt.Println("Error publishing:", err.Error())
}
}
}
The
handleClientno longer callsbroadcastto send messages to other connections. Instead every message received from a connection is published to a redis channel calledchatusing therdb.Publishmethod.
But how will we consume these messages from redis? To do that, let’s run a go routine that is called inside the
mainfunction.
CODEfunc main() {
redisHost := os.Getenv("REDIS_HOST")
redisPort := os.Getenv("REDIS_PORT")
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", redisHost, redisPort),
})
go func() {
ctx := context.Background()
sub := rdb.Subscribe(ctx, channel)
for {
message, err := sub.ReceiveMessage(ctx)
if err != nil {
fmt.Println("Error receiving message", err)
}
if message != nil {
broadcast([]byte(message.Payload))
}
}
}()
router := gin.Default()
// Rest of the main function
}
The go routine subscribes to the same redis channel
chatand reads all messages that are published to that channel using an infinitely running for loop. The message is then broadcasted to all websocket clients using thebroadcastfunction.
This is how the final
main.gofile will look like,
CODEpackage main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/redis/go-redis/v9"
)
const channel = "chat"
func main() {
redisHost := os.Getenv("REDIS_HOST")
redisPort := os.Getenv("REDIS_PORT")
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", redisHost, redisPort),
})
go func() {
ctx := context.Background()
sub := rdb.Subscribe(ctx, channel)
for {
message, err := sub.ReceiveMessage(ctx)
if err != nil {
fmt.Println("Error receiving message", err)
}
if message != nil {
broadcast([]byte(message.Payload))
}
}
}()
router := gin.Default()
router.StaticFile("/", "./static/index.html")
router.GET("/ws", serveWs(rdb))
err := router.Run()
if err != nil {
log.Fatalf("Unable to start server. Error %v", err)
}
log.Println("Server started successfully.")
}
func serveWs(rdb *redis.Client) func(c *gin.Context) {
return func(c *gin.Context) {
upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("Error in upgrading web socket. Error: %v", err)
return
}
go handleClient(conn, rdb)
}
}
var clients = make(map[*websocket.Conn]struct{})
type Message struct {
From string `json:"from"`
Message string `json:"message"`
}
func broadcast(msgBytes []byte) {
for conn := range clients {
conn.WriteMessage(websocket.TextMessage, msgBytes)
}
}
func handleClient(c *websocket.Conn, rdb *redis.Client) {
defer func() {
delete(clients, c)
log.Println("Closing Websocket")
c.Close()
}()
clients[c] = struct{}{}
for {
var msg Message
err := c.ReadJSON(&msg)
if err != nil {
log.Printf("Error in reading json message. Error : %v", err)
return
}
msgBytes, err := json.Marshal(msg)
if err != nil {
fmt.Println("Err marshaling", err.Error())
return
}
err = rdb.Publish(context.Background(), channel, string(msgBytes)).Err()
if err != nil {
fmt.Println("Error publishing:", err.Error())
}
}
}
Now, let’s understand how this solution works.
In conclusion, we've successfully addressed the scalability limitations of our chat application by integrating Redis pub-sub. By leveraging Docker for containerization and nginx for load balancing, we've achieved a robust and scalable architecture. With Redis acting as a message broker, our application now seamlessly distributes messages across multiple instances, ensuring real-time communication regardless of server distribution. This implementation not only enhances scalability but also lays the foundation for further optimizations and feature enhancements. Cheers to building resilient, real-time applications with Golang!
↗ Original-Artikel auf dev.to lesenVollständiger Original-BerichtAusführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
SOCIAL SHARE CARD GENERATOR