Skip to content

Commit

Permalink
feat: support server initiated heartbeats
Browse files Browse the repository at this point in the history
Before this change the websocket server would not have any heartbeat mechanism,
 this meant that unless the client proactively implements heartbeats the server
 would close the connection. From what I have seen most non-browser clients do
 not implement heartbeats (e.g filebeat, websocat), thus for those clients the
 connection would have been closed in 65 seconds.

The Websocket RFC does not set out a requirement which side should initiate
 heartbeats or that they are required. Most browsers have implemented heartbeats
 from client side, however it is not a must thus, in my opinion it is beneficial
 for the server to implement them, especially if the server closes connection if
 heartbeat is not received. As a consequence this changeset allows to support
 clients which aren't browsers.
  • Loading branch information
alberts-s committed Jun 16, 2024
1 parent 10238cd commit 41bc270
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]
### Added
- Support for non-browsers by implementing server initiated heartbeats (#39)
### Changed
### Fixed
### Docs
Expand Down
59 changes: 41 additions & 18 deletions internal/web/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,30 +36,47 @@ func newClient(conn *websocket.Conn, subType SubscriptionType, name string, cert

// Each client has a broadcastHandler that runs in the background and sends out the broadcast messages to the client.
func (c *client) broadcastHandler() {
writeWait := 60 * time.Second
pingTicker := time.NewTicker(30 * time.Second)

defer func() {
log.Println("Closing broadcast handler for client:", c.conn.RemoteAddr())

pingTicker.Stop()

_ = c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
_ = c.conn.WriteMessage(websocket.CloseMessage, []byte{})
_ = c.conn.Close()
}()

for message := range c.broadcastChan {
_ = c.conn.SetWriteDeadline(time.Now().Add(60 * time.Second))
for {
select {
case <-pingTicker.C:
log.Printf("Sent ping to %v\n", c.name)

w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
log.Printf("Error while getting next writer: %v\n", err)
return
}
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))

_, writeErr := w.Write(message)
if writeErr != nil {
log.Printf("Error while writing: %v\n", writeErr)
}
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
case message := <-c.broadcastChan:
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))

if closeErr := w.Close(); closeErr != nil {
log.Printf("Error while closing: %v\n", closeErr)
return
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
log.Printf("Error while getting next writer: %v\n", err)
return
}

_, writeErr := w.Write(message)
if writeErr != nil {
log.Printf("Error while writing: %v\n", writeErr)
}

if closeErr := w.Close(); closeErr != nil {
log.Printf("Error while closing: %v\n", closeErr)
return
}
}
}
}
Expand All @@ -73,17 +90,23 @@ func (c *client) listenWebsocket() {
ClientHandler.unregisterClient(c)
}()

readWait := 65 * time.Second

c.conn.SetReadLimit(512)
_ = c.conn.SetReadDeadline(time.Now().Add(65 * time.Second))
_ = c.conn.SetReadDeadline(time.Now().Add(readWait))

defaultPingHandler := c.conn.PingHandler()
c.conn.SetPingHandler(func(appData string) error {
// Ping received - reset the ping deadline to 65 seconds
_ = c.conn.SetReadDeadline(time.Now().Add(65 * time.Second))
// Ping received - reset the deadline
_ = c.conn.SetReadDeadline(time.Now().Add(readWait))
return defaultPingHandler(appData)
})
c.conn.SetPongHandler(func(string) error {
// Pong received
// Pong received - reset the deadline
_ = c.conn.SetReadDeadline(time.Now().Add(readWait))

log.Printf("Received pong from %v\n", c.name)

return nil
})

Expand Down

0 comments on commit 41bc270

Please sign in to comment.