2

For a goal to broadcast message from a goroutine to multiple http URL handlers, I am trying to register these http URL handlers, with below code in main.go:

type webSocketHandler func(http.ResponseWriter, *http.Request)

type threadSafeSlice struct {
    sync.Mutex
    handlers []*webSocketHandler
}

var sliceOfHandlers threadSafeSlice

func (slice *threadSafeSlice) push(handle *webSocketHandler) { //register

    slice.Lock()
    defer slice.Unlock()

    slice.handlers = append(slice.handlers, handle)
}

where forWardMsgToClient() is the http URL handler that need to be registered,

broadCastMessage() goroutine can broadcast message to multiple forWardMsgToClient() handlers, in the below code:

func main() {

    go broadcastMessage()
    http.HandleFunc("/websocket", forwardMsgToClient)
    http.ListenAndServe(":3000", nil)

}

func forwardMsgToClient(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    for {
         // Forward message to the client upon receiving a msg from publisher
    }
}

All the above code is in main.go

But the problem is, goroutine forwardMsgToClient() gets spawned for respective client after rw, e := l.Accept() call in ../go/src/net/http/server.go.

Reason to register(push()) http URL handler function(forwardMsgToClient()) is to make broadcastMessage() goroutine know the number of channels to create for all http URL handlers and delete the channel when un-registering http URL handler function(forwardMsgToClient()).


Bit nervous, if we need to modify /go/src/net/http/server.go to achieve this goal


How to register(push()) a http URL handler function forwardMsgToClient() in sliceOfHandlers.handlers?

overexchange
  • 15,768
  • 30
  • 152
  • 347
  • @CeriseLimón **1)** `broadcastmessage()` has no idea, when to `push`, because it does not know, when a new `accept()` connection occur that triggers `forwardMessageClient()`. Above query is assuming that code(`../go/src/net/http/server.go`) knows, when to push `forwardMessageToClient()` in `sliceOfHandlers`. Isn't it?. **2)** `broadcastMessage()`, just see the list of handlers already registered, with an approach(am seeking in above query) – overexchange Apr 05 '19 at 15:35
  • Let me know, if there is a different alternate approach to achieve the goal to broadcast message. – overexchange Apr 05 '19 at 15:36
  • @CeriseLimón yes. that message can be sent to all client by broadcasting message to each `/websocket` handler that is serving the client. Let me add the picture in query – overexchange Apr 05 '19 at 15:39
  • @CeriseLimón Can't `broadcastMessage()` send message to every URL handler? why should I revamp the complete app? there are multiple other URL handlers doing different job – overexchange Apr 05 '19 at 15:53
  • You are doing a pub/sub with a slice for your subscribers. But I think the element in the slice should be a channel not a handler. The pub will write to all the channels & subscribers will read from their individual sub channel. – colm.anseo Apr 05 '19 at 15:56
  • @colminator The pub does not know, how many subscribers are active at any time. Do you mean each subscriber(`forwardMsgToClient()`) suppose to append a new channel in slice of channels? so that pub can throw message in all channels created by subscribers? – overexchange Apr 05 '19 at 15:59
  • @overexchange - yes, as each subscribe comes in it will add to the slice. Not sure how you are IDing clients - but maybe a map would be better if there's a unique way to identify subscribes. This allows for easy unsubscribing when a client is no longer listening. – colm.anseo Apr 05 '19 at 16:07

1 Answers1

2

To broadcast a message to all connected websocket clients, do the following:

  • Add the connection to a collection on upgrade.
  • Remove the connection from a collection when the connection is closed.
  • Broadcast by iterating through the collection.

A simple approach is:

type Clients struct {
    sync.Mutex
    m map[*websocket.Conn]struct{}
}

var clients = Clients{m: map[*websocket.Conn]struct{}{}}

func (cs *Clients) add(c *websocket.Conn) {
    cs.Lock()
    cs.m[c] = struct{}{}
    cs.Unlock()
}

func (cs *Clients) remove(c *websocket.Conn) {
    cs.Lock()
    delete(cs.m, c)
    cs.Unlock()
}

func (cs *Clients) broadcast(message []byte) {
    cs.Lock()
    defer cs.Unlock()
    for c, _ := range m {
       c.WriteMessage(websocket.TextMessage, message)
    }
}

The handler adds and removes connections from the collection as follows:

func forwardMsgToClient(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        // handle error
    }
    defer c.Close()
    clients.add(c)
    defer clients.remove(c)

    // Read the connection as required by the package.
    for {
        if _, _, err := c.NextReader(); err != nil {
            break
        }
    }
}

To send a message to all connected clients, call clients.broadcast(message).

This simple approach is not production ready for a couple of reasons: it does not the handle the error returned from WriteMessage, broadcast can block on a stuck client.

For a more robust solution, see Gorilla chat example hub. The hub interposes a channel between the broadcaster and the connection, thus allowing the hub to broadcast without blocking. The go broadcastMessage() in the question corresponds to go hub.run() in the Gorilla example. The forwardMsgToClient handler in the question will create a *client and sent it to hub register channel on upgrade and send that *client to the hub unregister channel on disconnect. The *client has a channel that's pumped to the connection.

Charlie Tumahai
  • 113,709
  • 12
  • 249
  • 242
  • so, you are using ID of map as `websocket.Conn` of every handler that does `conn, err := upgrader.Upgrade(w, r, nil)` – overexchange Apr 05 '19 at 16:21
  • Yes, I mean, every request/websocket connection from client triggers its own `forwardMsgToClient()` as a goroutine, on server. Query edited with more code – overexchange Apr 05 '19 at 17:14
  • But I don't see the channel for each subscriber in your code, something like [this](https://github.com/gorilla/websocket/blob/master/examples/chat/hub.go#L14). I did not get this(`c.WriteMessage(websocket.TextMessage, message)`). `broadcast()` sends `message` through channel to each subscriber as mentioned [here](https://stackoverflow.com/questions/55538429/registering-an-http-url-handler-in-a-slice-of-handlers#comment97781888_55538429) – overexchange Apr 05 '19 at 17:30
  • The code in this answer shows the basic concept for solving the problem. Beyond that, the code in this answer no relation to the more robust code in the Gorilla example. Either follow the code here or the code in the Gorilla example. The latter is recommended. – Charlie Tumahai Apr 05 '19 at 17:35