1

In boost asio is there async_write_some for programmer to implement their own fine-grained logic similar to async_write in any fashion, etc:

class koebenhavn : public std::enable_shared_from_this<koebenhavn>
{
private:
    boost::asio::ip::tcp::socket socket;
public:
    ...
    void writing(std::vector<const_buffer> & buffers, boost::asio::io_context::strand & strand){
        /* io worker may call back at arbitrary amount of transferred bytes */
        socket->async_write_some(buffers, 
             boost::asio::bind_executor(strand, [self=shared_from_this(), &buffers, &strand](boost::system::error_code error, size_t transferred){
                 /* lambda instead of std::bind for specification if any possible overload */
                 self->wrote(buffers, strand, error);
             }));
    }
    void wrote(std::vector<const_buffer> & buffers, 
               boost::asio::io_context::strand & strand,
               boost::system::error_code error, size_t transferred){
               /* we need to advance buffers to send the rest bytes in reference */
               if(!error){
                 /* transfer occurred */
                 if(transferred){
  /* ----------------> here we need to construct new "cursor" of buffers based on the previous */
                   std::vector<const_buffer> cursor = advance(buffers, transferred);
  /* <---------------- repeat! */
                   writing(cursor, strand);
                 }
               }
    }
    ...
};

What I want is a simple function to advance the sequence of buffers for the next call to repeat. Now I suspect there isn't any present wheel for that in asio, is there?

There IS a solid reason not to use async_write in my case. Please leave it out if reply.

Y.Z
  • 626
  • 2
  • 9
  • 21

1 Answers1

1

What you expect exists. It's in the DynamicBuffer concept.

read|wrte_some member functions are low-level and do not take dynamic buffers (it takes a sequence of MutableBuffer). In fact, I maintain that using these functions is not what you want 99% of the time. E.g. see this remark:

Remarks

The read operation may not read all of the requested number of bytes. Consider using the async_read function if you need to ensure that the requested amount of data is read before the asynchronous operation completes

People often have wrong assumptions about IO packet delivery. Just use the async_read function instead, with your dynamic buffer:

Doing It Yourself

Of course, you can use a dynamic buffer (i.e. with read/write "cursors") with any lowlevel interfaces (that expect straight buffers). But then you have to use the prepare()/consume()/commit() interfaces to manage the cursors (and optionally allocations that may happen in the underlying storage).

You could compare this with iostreams: Instead of int x; std::cin >> x; you can very much use the underlying streambuf interface, but it will just be more work and error-prone.

Of course, sometimes you want that lowlevel interface (e.g. when you want to be able to know more detailed what packets arrive at what time), but I think that's the exception to the rule.

BONUS: Demo code

The simplest fix to your question code is to use the composed write operation:

void writing(std::vector<const_buffer> const& buffers, strand& strand)
{
    /* io worker may call back at arbitrary amount of transferred bytes */
    boost::asio::async_write(
        socket, buffers,
        bind_executor(strand, [self = shared_from_this(), strand]
            (error_code ec, size_t transferred) {
                std::cout << "Transferred " << transferred << " bytes "
                          << "(" << ec.message() << ")" << std::endl;
            }));
}

Here the "cursoring" is done by the library. No questions asked.

Notes:

  1. you might be more accepting of the buffer type, accepting any model of DynamicBuffer (v1/v2) and ConstBufferSequence:

    template <typename Buffers>
    void writing(Buffers&& buffers, strand& strand)
    {
        /* io worker may call back at arbitrary amount of transferred bytes */
        boost::asio::async_write(
            socket, std::forward<Buffers>(buffers),
            bind_executor(strand, [self = shared_from_this(), strand]
                (error_code ec, size_t transferred) {
                    std::cout << "Transferred " << transferred << " bytes "
                              << "(" << ec.message() << ")" << std::endl;
                }));
    }
    
  2. You could associate the executor with the IO object, e.g. like this:

    koebenhavn(boost::asio::io_context& ctx)
        : socket_(make_strand(ctx))
    { }
    

    Now you can use that (boost::asio::associated_executor(socket_) or socket_.get_executor() or just let the library do that by default when you initiate any async operation on that socket.

    template <typename Buffers>
        void writing(Buffers&& buffers)
    {
        /* io worker may call back at arbitrary amount of transferred bytes */
        async_write(
            socket, std::forward<Buffers>(buffers),
            [self = shared_from_this()](error_code ec, size_t transferred) {
                std::cout << "Transferred " << transferred << " bytes "
                          << "(" << ec.message() << ")" << std::endl;
            });
    }
    
  3. Notice things got simple? Time to complicate it back up. Your code has the tacit assumption that writing(...) will be called safely, i.e. from the strand. Since we don't know that for sure, consider posting or dispatching to the strand:

    dispatch( //
        socket_.get_executor(),
        [this, self, b = std::forward_as_tuple(buffers)]() mutable {
            async_write( //
                socket_, std::get<0>(b),
                [self](error_code ec, size_t transferred) {
                    std::cout << "Transferred " << transferred << " bytes "
                              << "(" << ec.message() << ")" << std::endl;
                });
        });
    

    Using dispatch has the advantage that the library may be able to run the task immediately if it detects that you're already on the strand.

Live Demo

Live On Coliru

#include <boost/asio.hpp>
#include <iostream>

class koebenhavn : public std::enable_shared_from_this<koebenhavn> {
  private:
    boost::asio::ip::tcp::socket socket_;
    using const_buffer = boost::asio::const_buffer;
    using error_code   = boost::system::error_code;
    using strand       = boost::asio::io_context::strand;

  public:
    koebenhavn(boost::asio::io_context& ctx)
        : socket_(make_strand(ctx))
    { }

    // io worker may call back at arbitrary amount of transferred bytes
    template <typename Buffers> void writing(Buffers&& buffers)
    {
        auto self = shared_from_this();
        dispatch( //
            socket_.get_executor(),
            [this, self, b = std::forward_as_tuple(buffers)]() mutable {
                async_write( //
                    socket_, std::get<0>(b),
                    [self](error_code ec, size_t transferred) {
                        std::cout << "Transferred " << transferred << " bytes "
                                  << "(" << ec.message() << ")" << std::endl;
                    });
            });
    }
};

int main() {
    boost::asio::io_context io;
    auto k = std::make_shared<koebenhavn>(io);

    boost::asio::streambuf sb;

    k->writing(sb);

    std::string s = "hello world\n";
    k->writing(boost::asio::buffer(s));

    std::array<float, 7>       ff{};
    std::vector<unsigned char> bb{{0, 1, 2, 3, 4}};

    k->writing(std::vector{
        boost::asio::buffer(ff),
        boost::asio::buffer(bb),
    });
}
sehe
  • 374,641
  • 47
  • 450
  • 633
  • Added [some demo code](http://coliru.stacked-crooked.com/a/aa3d10cd55ade9cc) signaling some improvements/attention points. Rereading your question you might have been aware of most of these (except perhaps the possible need for `dispatch`?). The short of it is: use DynamicBuffer concepts (or write the equivalent yourself). – sehe Sep 16 '21 at 14:13
  • Well explained and I really appreciate it. – Y.Z Sep 17 '21 at 08:41
  • Oh, late thought, if you go down the "I'll implement my lowlevel details myself", you might want to know about [buffer arithmetic](https://www.boost.org/doc/libs/1_77_0/doc/html/boost_asio/reference/buffer.html#boost_asio.reference.buffer.buffer_arithmetic) to at least keep a modicum of sanity :) – sehe Sep 17 '21 at 14:29
  • Thanks, I would. Moreover, as to the reason I thought to be solid at the low level, which in my case, is to enable developers to implement algorithm to profile the transmission for flexible QoS. Running io threads to handle `async_write` are literally executing off the stage, so... – Y.Z Sep 19 '21 at 16:06
  • Idea: look at tracking executors. A lot of inspiration can be gained from reading about BOOST_ASIO_ENABLE_HANDLER_TRACKING. that way you don't have to reimplement things but can instrument existing composed operations to get insight? – sehe Sep 19 '21 at 16:13
  • Well, I might choose to work around macros or scripting for a utmost homogeneity in language for library implement even it doesn't make any sense to the many others. But it was a valuable reference and noted. – Y.Z Sep 19 '21 at 17:03