c++websocketboostboost-asioboost-beast

Correct use of ping/async_ping with websockets


Let's say we have a websocket client based on boost::beast. At some point we want to send a ping frame to the server. To do this, boost::beast::websocket::stream offers two options:

  1. Use the ping method
  2. Use the async_ping method

I want to know how to use these methods correctly in an asynchronous environment.

  1. Can I safely call ping or async_ping if there is an outstanding (incompleted) async_write operation?
  2. Using ping seems preferable to async_ping because async_ping requires us to ensure that there are no outstanding ping, pong, async_ping, or async_pong operations when it is called. If I decide to use async_ping, what is the correct way to call it?
  3. If I decide to use ping (because ping is easy in use), how long can a synchronous ping call theoretically take? Could this be a bottleneck in an asynchronous application?

Which of these approaches is preferable to use in an asynchronous environment and how to implement it correctly?

Please note that I am working in a single-threaded but asynchronous environment.


Solution

  • You didn't ask, but mixing async_XXX and synchronous calls is not supported by beast::websocket::stream.

    1. No. [async_]ping() and [async_]close count as caller-initiated-write-operations. These must not be overlapped with other caller-initiated-write-operations.

      As far as automatic control frame behavior is concerned, these do not compete with caller-initiated-write-operations, this is documented here

      During read operations, Beast automatically reads and processes control frames. If a control callback is registered, the callback is notified of the incoming control frame. The implementation will respond to pings automatically. The receipt of a close frame initiates the WebSocket close procedure, eventually resulting in the error code error::closed being delivered to the caller in a subsequent read operation, assuming no other error takes place.

      However, these writes will not compete with caller-initiated write operations. For the purposes of correctness with respect to the stream invariants, caller-initiated read operations still only count as a read. This means that callers can have a simultaneously active read, write, and ping/pong operation in progress, while the implementation also automatically handles control frames.

    2. As mentioned above, in async context ping() is out of the question. async_ping should be queued. My recommendation: the best way to do ping is probably: not at all. I have yet to think of a reason why it would help. Also see e.g. How to use SO_KEEPALIVE option properly to detect that the client at the other end is down?

    3. You can't. How long ping() can take depends (only) on the peer implementation. See also Auto-fragment

    For some inspiration, beyond making a barrage of isolated questions consider looking at existing examples. E.g. I've recently answered a similar question to show how close operations (so, using async_close) are write operations as well, and how to elegantly combine them with a write queue: WebSocket async_close fails with "Operation canceled" in destructor (Boost.Beast)