Boost.Asio异步发送问题

6
我正在编写一个服务器应用程序,使用Boost.Asio。在我的代码中,async_send 要求调用者在数据发送成功之前一直保持对数据的所有权。这意味着我的代码(看起来像下面的代码)会失败,而且确实失败了,因为 data 不再是一个有效的对象。
void func()
{
    std::vector<unsigned char> data;

    // ...
    // fill data with stuff
    // ...

    socket.async_send(boost::asio::buffer(data), handler);
}

我的解决方案是做类似于这样的事情:

std::vector<unsigned char> data;

void func()
{        
    // ...
    // fill data with stuff
    // ...

    socket.async_send(boost::asio::buffer(data), handler)
}

但是现在我在想,如果我有多个客户端,我是否需要为每个连接创建一个单独的向量?

还是可以使用一个单一的向量?如果我能够使用这个单一的向量,如果我覆盖它内部的内容,那么是否会破坏我发送给所有客户端的数据?

7个回答

14

一个可能的解决方法是使用 shared_ptr 来持有您的本地 vector,并更改处理程序的签名以接收 shared_ptr ,以延长 data 的生命周期,直到发送完成(感谢 Tim 指出这一点):

void handler( boost::shared_ptr<std::vector<char> > data )
{
}

void func()
{
    boost::shared_ptr<std::vector<char> > data(new std::vector<char>);
    // ...
    // fill data with stuff
    // ...

    socket.async_send(boost::asio::buffer(*data), boost:bind(handler,data));
}

@Marlon:与性能有关的任何事情一样:这取决于情况,但它与涉及shared_ptr的任何其他解决方案一样有效(例如@Tim的解决方案)。 - Eugen Constantin Dinca
@Tim Sylvester - 我不明白为什么shared_ptr在函数退出时应该删除数据 - boost :: bind()仍然拥有<data>的副本。这不是与您的解决方案相同,而是将shared_ptr <Handler>传递给boost :: bind,我们传递了附加数据,其中包括shared_ptr <...>吗? - dimba
@dimba:在最初的版本中,它确实做了Tim所说的。 - Eugen Constantin Dinca
@Eugen Constantin Dinca - 那么现在你的答案包含可工作的代码了吗? :) - dimba

7
我通过将一个`shared_ptr`传递给我的数据处理程序来解决了类似的问题。由于asio在调用处理函数之前一直保持处理函数的引用,而处理函数保留了`shared_ptr`的引用,因此只要有一个打开的请求,数据就会保持分配状态。
编辑 - 这里是一些代码:
在这里,连接对象保持正在写入的当前数据缓冲区,因此`shared_ptr`指向连接对象,`bind`调用将方法函数附加到对象引用上,并且asio调用保持对象存活。
关键是每个处理程序必须使用另一个引用启动新的异步操作,否则连接将关闭。一旦连接完成或发生错误,我们就停止生成新的读写请求。一个注意事项是您需要确保在所有回调中检查错误对象。
boost::asio::async_write(
    mSocket,
    buffers,
    mHandlerStrand.wrap(
        boost::bind(
            &TCPConnection::InternalHandleAsyncWrite,
            shared_from_this(),
            boost::asio::placeholders::error,
            boost::asio::placeholders::bytes_transferred)));

void TCPConnection::InternalHandleAsyncWrite(
    const boost::system::error_code& e,
    std::size_t bytes_transferred)
{

不确定这是否回答了OP的问题。在我看来,OP描述的情况是当您的处理程序有几个并发(但尚未完成)的async_send操作时。在这种情况下,您的处理程序应该为每个并发的异步操作拥有相同数量的缓冲区。 - dimba
@dimba OP 询问如何从临时的基于堆栈的状态移动到全局状态。他的两个例子都只有一个缓冲区,这是不够的。将状态保持在异步回调中可以消除本地状态被销毁的问题,同时又不会增加需要显式管理全局状态(例如在受互斥保护的向量之间)的开销。 - Tim Sylvester

5

但是现在我在想如果我有多个客户端,我是否需要为每个连接创建一个单独的向量?

是的,虽然每个向量不需要在全局范围内。解决这个问题的典型方法是将 buffer 作为对象的成员保留,并将该对象的成员函数绑定到传递给 async_write 完成处理程序的函数对象上。这样,缓冲区将在异步写入的整个生命周期中保持在范围内。asio 示例 中充斥着使用 thisshared_from_this 绑定成员函数的用法。一般来说,最好使用 shared_from_this 来简化对象的生命周期,特别是面对 io_service:stop()~io_service()。尽管对于简单的示例,这种脚手架通常是不必要的。

上述的销毁序列允许程序使用shared_ptr<>来简化资源管理。当一个对象的生命周期与连接(或其他一些异步操作序列)的生命周期相关联时,该对象的shared_ptr将绑定到与其关联的所有异步操作的处理程序中。一个好的起点是async echo server,因为它非常简单。
boost::asio::async_write(
    socket,
    boost::asio::buffer(data, bytes_transferred),
    boost::bind(
        &session::handle_write,
        this,
        boost::asio::placeholders::error
    )
);

3
我一直在采用“TCP是流”的概念来处理数据。因此,我为每个连接创建了一个boost::asio::streambuf对象代表发送给客户端的内容。
与boost中的大多数示例类似,我有一个tcp_connection类,每个连接都有一个对象。每个对象都有一个成员变量boost::asio::streambuf response_;,当我想要向客户端发送数据时,只需执行以下操作:
std::ostream responce_stream(&response_);
responce_stream << "whatever my responce message happens to be!\r\n";

boost::asio::async_write(
    socket_,
    response_,
    boost::bind(
        &tcp_connection::handle_write,
        shared_from_this(),
        boost::asio::placeholders::error,
        boost::asio::placeholders::bytes_transferred));

2

除非您向所有客户端发送相同和恒定的数据(如提示消息),否则无法使用单个向量。这是由异步I/O的性质引起的。如果您正在发送,则系统将在其队列中保留指向您的缓冲区的指针以及一些AIO数据包结构。一旦它完成了一些先前排队的发送操作并且自己的缓冲区中有空闲空间,系统将开始为您的数据形成数据包,并将您的缓冲区的块复制到TCP帧的相应位置中。因此,如果您沿途修改缓冲区的内容,则会破坏发送到客户端的数据。如果您正在接收,则系统甚至可以进一步优化,并将您的缓冲区馈送到NIC作为DMA操作的目标。在这种情况下,可以节省大量CPU周期,因为它是由DMA控制器完成的。不过,这种优化可能仅在NIC支持硬件TCP卸载时才有效。

更新:在Windows上,Boost.Asio使用重叠WSA IO并通过IOCP完成通知。


2
Krit解释了数据损坏的原因,所以我会给您提供一些建议。我建议您为每个正在执行的发送操作使用单独的向量。您可能不想为每个连接使用一个向量,因为您可能希望在同一连接上按顺序发送多条消息,而无需等待前面的消息完成。请注意保留HTML标签。

1
你不能在TCP上进行同时发送,否则你的数据将交错和损坏。这只有在UDP消息中才有意义。 - Tim Sylvester
1
如果Tobbe所说的“同时”是指async_send调用是按顺序进行且不等待前一个调用完成,那么这实际上是有道理的。系统保证数据将按照提交操作的顺序发送。此外,始终在系统的“发送”队列中保留一些数据以实现TCP级别的优化(称为“延迟ACK”)也是有益的。这在快速网络上尤其明显。 - Krit
Krit,那就是我想表达的意思。谢谢你为我解释 :) - Tobias Furuholm
Krit:我借鉴了你的公式来改进答案,希望你不介意。谢谢! - Tobias Furuholm

0

每个连接都需要一个写缓冲区,其他人建议使用每个连接的向量,就像您最初的想法一样,但是我建议使用字符串向量来简化您的新方法。

Boost.ASIO围绕使用字符串与其缓冲区进行写入构建了一些特殊情况,这使得它们更易于使用。

只是一个想法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接