snapview/tungstenite-rs

Send/receive messages in parts

Closed this issue · 7 comments

Initially started at sdroege/async-tungstenite#108, but apparently it requires changes here too:

I have the use case where I have to send a message of X bytes, and I know X in advance, but it is fairly large so I'd like to not have to have the entire message in memory beforehand. Instead, I'd like to lazily generate it in small chunks as they are being sent. (Of course there will need to be a check afterwards that exactly as many bytes as advertised were sent)

Would such a feature be feasible?

Similarly, but also less importantly, what about that feature for the reading direction?

(Note that I don't know very much about the WebSockets protocol in general.)

To me, it sounds like you want to use the so-called concept of fragmentation in WebSockets from the RFC 6455, i.e. the ability to send chunks for the message as they get available instead of accumulating and sending the full message. We merged the support of this feature about a year ago. If you would like to use it, feel free to check the discussion thread here: #250

Hope this helps 😉

Thanks. Knowing the real name of the feature is really helpful.

I read things up and I think I still have questions:

  • As far as I can tell sending fragmented messages is as easy as sending correct Message::Fragment(…) messages.

  • On the reading end, fragmented messages get concatenated automatically into whole messages. Is there a way to opt out of this, in order to actually receive the fragmented parts?

    • Conversely, does sending a message larger than max_frame_size automatically split it into multiple fragments?
  • The WebSocket specification says:

    In the absence of any extension, a receiver
    doesn't have to buffer the whole frame in order to process it. For
    example, if a streaming API is used, a part of a frame can be
    delivered to the application.

    is there currently a way to implement this behavior with tungstenite?

So the current high level API that we expose to the user, deals with messages (i.e. a combination of one or more frames). We decided to use this concept since it's the easiest one to comprehend (especially for people who are not familiar with the WebSockets) and also easy to work with since tungstenite takes over the part that ensures sticking to the RFC (i.e. auto-reply to ping messages with a pong message, proper handling of the close "handshake" etc) that otherwise would have been implemented by the user (the absence of such an implementation would produce wrong client/server software that does not stick to the RFC).

However, the internals of the tungstenite work with frames, i.e. we first accumulate a frame and then return it to the higher level wrappers that further perform checking the opcodes, applying masks, do auto-reply to pings and connection close state machine etc. This means that theoretically we could refactor the core of the tunsgstenite by separating the logic that works with frames and provide a separate (perhaps feature-guarded) API for a more low-level work with frames. This would be a bit less ergonomic and would require some knowledge of WebSockets from the user, but it is certainly possible to implement.

Conversely, does sending a message larger than max_frame_size automatically split it into multiple fragments?

Currently, the only thing that max_frame_size affects is the size of the frame payload when reading the frame from the socket. If the size of the payload exceeds the max_frame_size, we return CapacityError::MessageTooLong error.

Thanks again for the clarification. Splitting up large messages into multiple messages is sadly not an option for me, but enforcing a maximum frame size and then working with that is something that feels feasible.

  • I think the current APIs already allow me to send individual message frames, although I'd appreciate an option to have a tx_max_frame_size option that automatically splits large payloads for me.
  • I would need some way to read messages in their individual frames as they arrive. Since Frame is already part of Message, I think an opt-in to getting the individual frames might work. This could either be a global configuration option for the entire connection, or a dedicated method for advanced users (maybe read_message_raw or read_message_frame)

I think the current APIs already allow me to send individual message frames, although I'd appreciate an option to have a tx_max_frame_size option that automatically splits large payloads for me.

Yeah, probably it would make more sense in the future indeed. So far we did not add it since our original method of the Message API was pretty high-level and simple to use without too many technicalities given to the user, since we assumed that an internal details on how the message is split and sent are the implementation details that should not bother the user. Whereas when someone wants to send fragmented messages, they probably know what they are doing and want more control over the process of sending anyway.

I would need some way to read messages in their individual frames as they arrive. Since Frame is already part of Message, I think an opt-in to getting the individual frames might work. This could either be a global configuration option for the entire connection, or a dedicated method for advanced users (maybe read_message_raw or read_message_frame)

Yep, that would be also possible, it's just currently we don't expose such an API. The thing is that if we support this sort of the API, it would be an "either/or" option, i.e. you can either read messages from the WebSocket or read frames (so these would be different data types), otherwise one could read a single frame (and let's say this is one of the "continue" frames, not the full message) and then calls to the read message function which immediately poses a question of whether we should return the fully accumulated message (if any) including the "continue frame" that we returned before or if we should rather return a message without this frame (which would be corrupted/incomplete message then). That's why we decided to not introduce this API unless there is a need for that in the future :)

I totally agree with your design decisions, as most users indeed do not case about the WebSocket internals. It is only when having to write some specific server software (e.g. a relay) that those details become relevant because of performance reasons. Therefore, I'd be fine with an "either/or" API.