websocket icon indicating copy to clipboard operation
websocket copied to clipboard

[question] How to create an onMessage handler on server side?

Open WillNilges opened this issue 2 years ago • 3 comments

Describe the problem you're having

A clear and concise description of what the bug is.

I'm writing a server for an app that opens a websocket connection between itself and a given client. It's trivially easy to send messages to the client with this library, but I'm having trouble figuring out how to implement an onMessage() function or something like that on the server. The client needs to be able to send a sort of "cancel" or "nevermind" message to the server. I suppose it would also work if I could just close the socket on the client side and that would tell the server to do something.

Versions

Go version: go version

1.18

package version: run git rev-parse HEAD inside the repo

af47554f343b4675b30172ac301638d350db34a5

"Show me the code!"

A minimal code snippet can be useful, otherwise we're left guessing! Hint: wrap it with backticks to format it

Simple JavaScript function as the client

function knockSocket() {
  url = 'ws://localhost:8080/knock/socket';
  ws = new WebSocket(url);

  ws.onopen = function(){
    console.log("Connected to websocket :)")
    resetRequestModal();
    openRequestModal();
    cancelLink.addEventListener("click", () => {
      socketNevermind(ws);
    });
  }

  ws.onmessage = function(msg) {
    console.log("Got message!" + msg.data);
    data = JSON.parse(msg.data);
    if (data.Event === "COUNTDOWN") {
      // Apply a 1 second offset to make animation look good.
      updateTimeoutBar(data.CurrentTime - 1, data.MaxTime - 1);
    } else if (data.Event === "TIMEOUT") {
      timeoutCounter.hidden = true;
      timeoutBar.hidden = true;
    }
  }
}

Relevant Go code


func wsTimeoutHandler(w http.ResponseWriter, r *http.Request) {
	timeoutEnv, _ := os.LookupEnv("LMI_TIMEOUT")
	timeout, _ := strconv.Atoi(timeoutEnv)

	fmt.Println("Somebody has knocked. Timeout is ", timeout, "s")

	conn, err := wsUpgrader.Upgrade(w, r, nil)
	if err != nil {
		fmt.Println("Failed to set websocket upgrade: %+v", err)
		return
	}

	// Defer the client to a goroutine.
	// 1 second offset to make the timeout modal look better.
	go doCountdown(conn, timeout+1)
}

func doCountdown(conn *websocket.Conn, timeout int) {
	defer conn.Close()
	for i := timeout; i > 0; i-- {
		// _, clientMessageBytes, _ := conn.ReadMessage()
		// clientMessage := fmt.Sprintf("%s", clientMessageBytes)
		// if clientMessage == "NEVERMIND" {
		// 	fmt.Println("Got NEVERMIND from client")
		// 	return
		// }
		message, _ := json.Marshal(KnockObject{"COUNTDOWN", i, timeout})
		conn.WriteMessage(websocket.TextMessage, message) // json go brrr
		time.Sleep(1 * time.Second)
	}

	timeoutMessage, _ := json.Marshal(KnockObject{"TIMEOUT", 0, timeout})
	conn.WriteMessage(websocket.TextMessage, timeoutMessage)
}

func knockHandler(c *gin.Context) {
	wsTimeoutHandler(c.Writer, c.Request)
}

Repo can be found here: https://github.com/WillNilges/letmein2/tree/willnilges/websocket_timeout

WillNilges avatar Jul 24 '22 21:07 WillNilges

Right now what I'm doing is this:

func watchForNvm(conn *websocket.Conn) {
	_, message, err := conn.ReadMessage()
	if err != nil {
		log.Println("read:", err)
		return
	}
	if string(message) == "NEVERMIND" {
		fmt.Println("Got NEVERMIND!")
	}
}

Not sure if this is "clean," or "good?" It seems to me like the goroutines I used might not quit if something happens to the websocket externally. Will this gracefully quit if the websocket closes? Will this break doCountdown() or vice versa?

WillNilges avatar Jul 24 '22 21:07 WillNilges

I think this'll work:

func knockHandler(c *gin.Context) {
	wsTimeoutHandler(c.Writer, c.Request)
}

func wsTimeoutHandler(w http.ResponseWriter, r *http.Request) {
	timeoutEnv, _ := os.LookupEnv("LMI_TIMEOUT")
	timeout, _ := strconv.Atoi(timeoutEnv)

	fmt.Println("Somebody has knocked. Timeout is ", timeout, "s")

	conn, err := wsUpgrader.Upgrade(w, r, nil)
	if err != nil {
		fmt.Println("Failed to set websocket upgrade: %+v", err)
		return
	}
	conn.SetReadDeadline(time.Now().Add(time.Duration(timeout+2) * time.Second))

	// 1 second offset to make the timeout modal look better.
	go knockDoCountdown(conn, timeout+1)
	go knockWatchForNvm(conn)
}

func knockDoCountdown(conn *websocket.Conn, timeout int) {
	defer conn.Close()
	for i := timeout; i > 0; i-- {
		message, _ := json.Marshal(KnockObject{"COUNTDOWN", i, timeout})
		conn.WriteMessage(websocket.TextMessage, message) // json go brrr
		time.Sleep(1 * time.Second)
	}

	timeoutMessage, _ := json.Marshal(KnockObject{"TIMEOUT", 0, timeout})
	conn.WriteMessage(websocket.TextMessage, timeoutMessage)
}

// TODO: Is this returning properly?
func knockWatchForNvm(conn *websocket.Conn) {
	_, message, err := conn.ReadMessage()
	if err != nil {
		log.Println("knockWatchForNvm:", err)
		log.Println("Exiting knockWatchForNvm")
		return
	}
	if string(message) == "NEVERMIND" {
		fmt.Println("Got NEVERMIND!")
	}
}

The knockWatchForNvm will kill itself when the socket closes. ✅

WillNilges avatar Jul 25 '22 03:07 WillNilges