snapview / tokio-tungstenite

Future-based Tungstenite for Tokio. Lightweight stream-based WebSocket implementation
MIT License
1.88k stars 236 forks source link

How can I send pong frames as heartbeats from a client? #329

Closed hibnikar closed 6 months ago

hibnikar commented 6 months ago

Suppose I have a client that listens to messages from WS server and simply persits them somewhere else. Server periodically sends ping frames and tungstenite-rs automatically responds with proper pong frames.

However, now the server has introduced a requirement to periodically send pong frames with some custom content as a way to show that the client is still interested in updates.

I've quickly found this comment in tungstenite-rs repo where you are advised to invoke write_pending()? to make sure default pong response is not overwritten with the custom one and only then sending custom pong frame.

But how do i do this in tokio-tungstenite?

Is doing something like

type SocketStream = WebSocketStream<MaybeTlsStream<TcpStream>;

async fn send_custom_pong(&mut sink: SplitSink<SocketStream>, Message>) -> Result<(), WsError> {
    sink.flush().await?;

    Ok(sink.send(Message::Pong(create_custom_pong()).await?)
}

would work? Would this work in case the code sends a lot of data to the server or is there a better way to handle this?

agalakhov commented 6 months ago

You can just send Pong just like you send normal messages, there is nothing special about it.

hibnikar commented 6 months ago

Welp, mentioned issue says

If using through tokio-tungstenite or other code that splits reader and writer tasks, things become complicated, because now you have to communicate from your reader that received the ping that your writer should call write_pending before sending the next pong, and through tokio-tungstenite write_pending isn't exactly accessible.

Is this no longer valid?

agalakhov commented 6 months ago

This is valid, but this is not so different from other (non-ping) communications. Reading includes write_pending(), so if you read all the time, it will be called. Just make sure it happens.

daniel-abramov commented 6 months ago

@hibnikar I've just read your question and checked the aforementioned issue. If I got your use case right, then yes—the solution that you proposed should achieve the behavior you want. However, beware that your task must not yield between flush() and send() if there is a reader task working in parallel (otherwise, you'll run into a problem that was described in the issue that you linked).

Unfortunately, I can't think of a better way to handle this situation without some drawbacks. Ideally, we would need to solve the original issue to allow for the handling of such cases in a more elegant way. Currently, close and pong frames are handled in a special way (which is good if you want a simple/typical behavior but not particularly useful when your logic is a bit more sophisticated).

Hope this helps.