1

I am working on the application with chat module which implemented using Angular in Frontend and Golang in Backend. We used RxJS package for websocket in the frontend, Fiber (framework) websocket (Includes Gorilla websocket) in backend. I have few queries here.

The websocket connections are getting closed some time randomly. Everything seems to be working fine in localhost, but when we move the code to Dev/QA server, it works fine for sometime, later it stops working, messages send will not reach to server / will not reach to recipients. We found that the websocket is getting closed due to some reasons. We thought that timeout in Load balancer cause the issue, we increased the timeout upto 3600 seconds. Also made some config changes in ngnix. But still we have the same issue.

To keep the websocket connection alive we implemented the PING/PONG mechanism. The server will send the PING frame, clients will respond as PONG. When I googled, there are some articles suggested to send the PING frame from clients and server will respond to it as PONG. I am bit confused which way would be feasible and better way to implement the PING/PONG mechanism, Is it from clients or server side?

When I debugged the code more, I got to know that the connections are being randomly changed in the Hub.Clients map which we use it for keeping the connections. So when we send a message, only few will receive it and few will not receive the messages. But we tried client side when we see that connection seems to be active. So bit confused where could be the issue?

Hub is passed as param to all the supporting functions wherever needed. Can we keep this Hub.Clients as global map so that it can be accessed from everywhere? Is there any restrictions on this? ALso can we store the connections in the DB and access it?

Also, Is it okay to use non-80 port for websockets? Will there be any advantage over port 80 than other ports?

Also any specific things to be handled in the infrastructure level?

Here the code what I use:

Angular Code

connectWebsocket() {

    closeSubject.subscribe(error => {
        this.connectWebsocket();   
    });

    var websocketSubjectObj = {
        url: this.websocketUrl,
        closeObserver: closeSubject,
        openObserver: {
        next: () => {        
            console.log("Connection established");
        }
        }
    };
    this.wsSubject = webSocket(websocketSubjectObj);
    var thisTemp = this;
    this.wsSubject.asObservable().pipe(
        retryWhen(
        genericRetryStrategy({
            scalingDuration: 5000,            
            maxRetryAttempts: 10
        })
        ),
        catchError(error => of(error))
    ).subscribe(dataFromServer => {                
        thisTemp.dataObserver.next(dataFromServer);
    }, err => {
        console.log("Error",err)
        
    }, () => {
        console.log("Web socket disconnected")
    });

}

Go Code

// Routes/route.go
// ---------------

Hub := chatHandler.NewConnHub()
go chatHandler.RunConnHub(Hub)
app.Use("/ws", func(c *fiber.Ctx) error {   
    if websocket.IsWebSocketUpgrade(c) { 
        return c.Next()
    }
    log.Println("Error on websocket connection", fiber.StatusUpgradeRequired)
    return c.SendStatus(fiber.StatusUpgradeRequired)
})
app.Get("/ws/:uniq_id", websocket.New(func(socket *websocket.Conn) {    
    chatHandler.ChannelWebSocketConnRedirector(Hub, socket) // chatHandler is another package where we have written the core logic
}))

// Chat/chat_handler.go
// --------------------

type ClientModel struct {
    Socket              *websocket.Conn
    id                  string  
    Mu                  sync.Mutex   
}

var clientsMutex ClientModel

func NewConnHub() *config.ConnHub {
    return &config.ConnHub{
        Notification: make(chan *config.Notification),
        Register:     make(chan *config.ClientModel),
        Unregister:   make(chan *config.ClientModel),
        Clients:      make(map[string]*config.ClientModel),     
    }
}

func ChannelWebSocketConn(hub *model.ConnHub, user *model.Client) {
    // When the function returns, unregister the client and close the connection
    defer func() {
        hub.Unregister <- user  
        user.Socket.Close()
    }()
    
    t := time.Time{}
    user.Socket.SetReadDeadline(t) 
    user.Socket.SetPongHandler(func(string) error {  
        log.Println("Got the pong")      
        user.Socket.SetReadDeadline(t)
        return nil
    })
    // Register the client  
    hub.Register <- user

    for {
        messageType, message, err := user.Socket.ReadMessage()     
        if err != nil {
            log.Println("this is unexpected close error", err)
            return // Calls the deferred function, i.e. closes the connection on error
        }
        if messageType == websocket.TextMessage {           
            log.Println("Received some message", string(message))
            hub.Notification <- &model.Notification{Client: user, Message: string(message)}         
        } else {
            log.Println("Received the messages in other formats")
        }
    }
}

func RunConnHub(hub *config.ConnHub) {   
    for {
        select {
        case client := <-hub.Register:
            // Add the user to the connection list
             clientsMutex.Lock()
             Hub.Clients[client.id] = client
             clientsMutex.Unlock()
        case client := <-hub.Unregister:
            // Remove the user from the connection list
             delete(Hub.Clients, client.id)
        case notification := <-hub.Notification:
             // Store the message
             parsedMessage, err := gabs.ParseJSON([]byte(notification.Message))
             message = parsedMessage.Path("message").Data().(string)
             room = parsedMessage.Path("room_id").Data().(primitive.ObjectID)
             activeUsers = getActiveUsersFromConnections(room,Hub.Clients) // Here will fetch the users from room and match with Hub.clients and get the active users
            support.BroadcastMessage(activeUsers,message) // Called from support functions
        }

    }
}

// Support/support.functions.go
// -----------------------------
BroadcastMessage(activeUsers,message){
        for _, ec := range activeUsers {
        ec.Mu.Lock()
        defer ec.Mu.Unlock()            
        t := time.Time{}
        ec.Socket.SetWriteDeadline(t)
        err = ec.Socket.WriteMessage(messageType, []byte(message))
        if err != nil {     
            ec.Socket.Close()
        } 
    }
} 
Brits
  • 14,829
  • 2
  • 18
  • 31
Ramarasu R
  • 55
  • 5
  • 1
    Are you sure that a `client.id` will only have one connection? Range of potential issues (if you are only accessing `Hub.Clients` from `RunConnHub` then you don't need the `Mutex` otherwise use it for ALL access, why always set 0 deadline `SetReadDeadline(time.Time{})`?, using `defer` in a loop is usually an error etc). Unfortunately I suspect the main issue is in in code you did not provide - Please include a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) and aim to ask [one question](https://meta.stackexchange.com/a/222741) at a time. – Brits Aug 06 '22 at 20:37
  • Thanks for your reply. I was literally stuck, so asked the many questions. Will do the same as you mentioned. – Ramarasu R Aug 07 '22 at 00:34
  • Added the mutex as I got the concurrent read,write issue as more than one users tries to connect, then it tries to access the same Map. Any suggestions on this? – Ramarasu R Aug 07 '22 at 00:42
  • 1
    Either limit changes to the map to the `RunConnHub` (guaranteeing no simultaneous access - this keeps things simple) or ensure you always hold the mutex before accessing `Hub.Clients` (e.g. `delete(Hub.Clients, client.id)`, `activeUsers =...`). – Brits Aug 07 '22 at 00:46
  • Thanks. Does lock wait to complete the previous process to release the resource? Or Will it be ignored. For ex, when user A & B tries to access the same map Hub.clients, then If I lock it, any one user will be added to it or both of them? – Ramarasu R Aug 07 '22 at 01:11
  • 1
    Tips for diagnosing the problem: Run the application with the [race detector](https://go.dev/doc/articles/race_detector). (2) Log the error returned from ec.Socket.WriteMessage. – Charlie Tumahai Aug 07 '22 at 02:02
  • 1
    I'm talking about `clientsMutex` which you appear to be using to protect the collection (slice) of clients. I believe that you are talking about `ClientModel.Mu` which protects an individual client (this is only referenced once in the code shown so I can't say much about it). Note that Stack overflow comments are not really intended for discussions (consider [another resource](https://go.dev/help) if you want a discussion and perhaps watch [Rob Pike's talk](https://go.dev/blog/io2013-talk-concurrency)). – Brits Aug 07 '22 at 02:14

0 Answers0