1

我基于 boost coroutine echo server example 制作了我的服务器,只是接收和写回一些数据。它在向客户端写入数据时崩溃,更奇怪的是,它只在使用多核时崩溃。

这是服务器,它读取 4 个字节并在 1 秒内写回“OK”作为超时:

#include <winsock2.h>
#include <windows.h>

#include <iostream>
using namespace std;

#include <boost/thread/thread.hpp>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
using namespace boost;
using namespace boost::asio;
using namespace boost::asio::ip;

#define SERVER_PORT 1234
#define DATA_LEN_4 4

#define TIMEOUT_LIMIT 1 // second

struct session : public std::enable_shared_from_this<session>
{
    tcp::socket socket_;
    boost::asio::steady_timer timer_;
    boost::asio::strand<boost::asio::io_context::executor_type> strand_;

    explicit session(boost::asio::io_context& io_context, tcp::socket socket)
    : socket_(std::move(socket)),
      timer_(io_context),
      strand_(io_context.get_executor())
    { }

    void go()
    {
        auto self(shared_from_this());
        boost::asio::spawn(strand_, [this, self](boost::asio::yield_context yield)
        {
            try
            {
                timer_.expires_from_now(std::chrono::seconds(TIMEOUT_LIMIT));

                // recv data
                string packet;
                packet.resize(DATA_LEN_4); // alloc memory

                size_t received_len = 0;

                // read data
                {
                    size_t rs;
                    while(received_len < DATA_LEN_4) { // recv 4 bytes
                        boost::system::error_code ec;

                        rs = socket_.async_read_some(
                            boost::asio::buffer((char*)(packet.c_str()+received_len), DATA_LEN_4-received_len), yield[ec]);
                        if(ec==boost::asio::error::eof)
                            break; //connection closed cleanly by peer
                        else if(ec) {
                            throw "read_fail";
                        }
                        received_len += rs;
                    }
                }
                if(received_len < DATA_LEN_4) {
                    throw "recv too short, maybe timeout";
                }
                // write back "OK"
                {
                    boost::system::error_code ecw;
                    boost::asio::async_write(socket_, boost::asio::buffer(string("OK")), yield[ecw]);
                    if(ecw==boost::asio::error::eof)
                        return; //connection closed cleanly by peer
                    else if(ecw)
                        throw "write_fail"; // some other error
                }
            }
            catch (const char* reason) 
            {
                printf("exception reason: %s\n", reason);
                boost::system::error_code ecw;

                /*
                 * Question 1: why this 'async_write' line causes crash?
                 */
                // write the error reason to client
                boost::asio::async_write(socket_, boost::asio::buffer(string(reason)), yield[ecw]);

                socket_.close();
                timer_.cancel();
            }
            catch (...)
            {
                printf("unknown exception\n");
                socket_.close();
                timer_.cancel();
            }
        });

        boost::asio::spawn(strand_, [this, self](boost::asio::yield_context yield)
        {
            while (socket_.is_open())
            {
                boost::system::error_code ignored_ec;
                timer_.async_wait(yield[ignored_ec]);
                if (timer_.expires_from_now() <= std::chrono::seconds(0))
                    socket_.close();
            }
        });
    }
};

int main() {
    boost::asio::io_context io_context;

    boost::asio::spawn(io_context, [&](boost::asio::yield_context yield)
    {
        tcp::acceptor acceptor(io_context,
        tcp::endpoint(tcp::v4(), SERVER_PORT));

        for (;;)
        {
            boost::system::error_code ec;

            tcp::socket socket(io_context);
            acceptor.async_accept(socket, yield[ec]);
            if (!ec) 
                std::make_shared<session>(io_context, std::move(socket))->go();
        }
    });

    /*
     * When run on 1 CPU, it runs fine, no Crash 
     */
    // io_context.run();

    /*
     * Question 2:
     * But when run on multiple CPUs, it Crashes !!!
     * Why?
     */
    auto thread_count = std::thread::hardware_concurrency();
    boost::thread_group tgroup;
    for (auto i = 0; i < thread_count; ++i)
        tgroup.create_thread(boost::bind(&boost::asio::io_context::run, &io_context));
    tgroup.join_all();
}

请注意,4 字节数据包1 秒超时只是为了说明问题,真实服务器使用大数据包可能会在网络状况不佳时导致超时。为了模拟这一点,客户端每秒写入 1 个字节以触发服务器上的读取超时。

客户端:

#include <iostream>
#include <boost/asio.hpp>
using namespace std;

using boost::asio::ip::tcp;

#define SERVER "127.0.0.1"
#define PORT "1234"

int main() {
    boost::asio::io_context io_context;

    unsigned i = 1; 
    while(1) {
        try {
            tcp::socket s(io_context);
            tcp::resolver resolver(io_context);
            boost::asio::connect(s, resolver.resolve(SERVER, PORT));

            // to simulate the bad network condition,
            // write 4 bytes in 4 seconds to trigger the receive timeout on server, which is 1 second
            for(int i=0; i<4; i++) { 
                boost::asio::write(s, boost::asio::buffer(string("A")));
                std::this_thread::sleep_for(std::chrono::seconds(1)); // sleep 1 second
            }

            // read echo
            char x[64] = {0};
            s.read_some(boost::asio::buffer(x, sizeof(x)));
            cout << i++ << ". received: " << x << endl;
        } catch (...) {
            cout << i++ << " exception" << endl;
        }
    }

    return 0;
}

问题一

为什么这条线会导致崩溃?

boost::asio::async_write(socket_, boost::asio::buffer(string(reason)), yield[ecw]);

问题 2

为什么服务器在 1 cpu 上运行时不会崩溃:io_context.run();
并在多个 CPU 上使用thread_group?

我的环境:Win10-64bit、boost-1.71.0-64bit、VisualStudio-2017-Community

4

1 回答 1

3

问题 1

ba::async_write(socket_, ba::buffer(string("OK")), yield[ecw]);

async_write这会调用未定义的行为,因为您将临时字符串作为缓冲区传递,但异步操作(根据定义)在调用返回之前并未完成。

因此,缓冲区是对堆栈上被破坏的东西或现在存在于其中的任何东西的过时引用。

发送缓冲区在逻辑上将成为self对象的一部分,以获得更合适的生命周期。或者,由于您正在执行协程并且无论如何您都将结束会话,因此只需使用write而不是async_write.

问题2

那是因为未定义的行为是未定义的行为任何事情都有可能发生

未问的

  • 而不是与, 或与适当的完成条件一起使用read_somereadtransfer_exactly(DATA_LEN_4)read_until

  • 而不是buffer(reserved_string)你可以dynamic_buffer

  • 无需抛出神奇的字符串,您只需捕获system_error代码表示发生了什么条件的位置:

    try {
        timer_.expires_from_now(std::chrono::seconds(TIMEOUT_LIMIT));
    
        // read data
        std::string packet;
        auto received_len = ba::async_read(socket_,
                ba::dynamic_buffer(packet),
                ba::transfer_exactly(DATA_LEN_4), yield);
    
        assert(received_len == DATA_LEN_4); // guaranteed
    
        // write back "OK"
        ba::write(socket_, ba::buffer("OK"s));
    }
    catch (boost::system::system_error const& e) {
        if (e.code() == ba::error::operation_aborted)
            std::cout << "canceled (timeout)" << std::endl;
        else if (e.code() == ba::error::eof)
            std::cout << "eof" << std::endl;
        else throw std::runtime_error(e.code().message());
    }
    
  • 所以,现在你可以用你的通用异常处理块来包装它:

    try {
        // ...
    } catch (std::exception const& e) {
        std::cout << "exception: " << std::quoted(e.what()) << std::endl;
    
        boost::system::error_code ignore;
        ba::async_write(socket_, ba::buffer(std::string(e.what())), yield[ignore]);
    
        socket_.close();
        timer_.cancel();
    }
    

    但!

    1. 通知您的客户是否有用甚至是明智的,这似乎非常值得怀疑
    2. 无论如何,不​​捕获 coro 中的异常都会破坏self实例,因此您可以简单地让它逃脱

计时器

  • 时间完成error_code已经表示计时器是过期还是取消:

    while (socket_.is_open()) {
        boost::system::error_code ec;
        timer_.async_wait(yield[ec]);
    
        if (ba::error::operation_aborted != ec) // timer was not canceled
            socket_.close();
    }
    
  • 但是请注意,会话 coro 的常规返回路径不会调用.cancel(). time_这将导致套接字保持打开另一个<1s,直到计时器到期。

例外

如果你想让异常从 coros 中逃逸(你可以,并且你应该考虑它会发生),你必须通过处理异常来改进线程循环:boost::asio::io_service::run() 是否应该抛出异常被抓?

服务器的建议代码

结合 coros,大大简化所有条件处理:

#include <iostream>
#include <iomanip>

#include <boost/thread/thread.hpp>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/scope_exit.hpp>

using namespace std::literals;
namespace ba = boost::asio;
using ba::ip::tcp;

static constexpr unsigned short SERVER_PORT = 1234;
static constexpr std::size_t    DATA_LEN_4 = 4;
static constexpr auto           TIMEOUT_LIMIT = 1s;

struct session : public std::enable_shared_from_this<session>
{
    tcp::socket socket_;
    ba::steady_timer timer_;
    ba::strand<ba::io_context::executor_type> strand_;

    explicit session(ba::io_context& io_context, tcp::socket socket)
    : socket_(std::move(socket)),
      timer_(io_context),
      strand_(io_context.get_executor())
    { }

    void go() {
        ba::spawn(strand_, [this, self = shared_from_this()](ba::yield_context yield) {

            spawn(yield, [this, self](ba::yield_context yield) {
                timer_.expires_from_now(TIMEOUT_LIMIT);
                while (socket_.is_open()) {
                    boost::system::error_code ec;
                    timer_.async_wait(yield[ec]);
                    if (ba::error::operation_aborted != ec) // timer was not canceled
                        socket_.close();
                }
            });

            try {
                // read data
                std::string packet;
                ba::async_read(socket_,
                        ba::dynamic_buffer(packet),
                        ba::transfer_exactly(DATA_LEN_4), yield);

                // write back "OK"
                ba::write(socket_, ba::buffer("OK"s));
            }
            catch (boost::system::system_error const& e) {
                if (e.code() == ba::error::operation_aborted)
                    std::cout << "canceled (timeout)" << std::endl;
                else if (e.code() == ba::error::eof)
                    std::cout << "eof" << std::endl;
                else // throw std::runtime_error(e.code().message());
                    std::cout << "other: " << e.code().message() << std::endl;
            }

            socket_.close();
            timer_.cancel(); // cancel the other coro so we don't keep the session alive
        });
    }
};

int main() {
    ba::io_context io_context;

    ba::spawn(io_context, [&](ba::yield_context yield) {
        tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), SERVER_PORT));

        for (;;) {
            boost::system::error_code ec;

            tcp::socket socket(io_context);
            acceptor.async_accept(socket, yield[ec]);
            if (!ec) 
                std::make_shared<session>(io_context, std::move(socket))->go();
        }
    });

    boost::thread_group tgroup;
    for (auto i = 0u; i < std::thread::hardware_concurrency(); ++i)
        tgroup.create_thread([&io_context] {
            for (;;) {
                try { io_context.run(); break; } // exited normally
                catch (std::exception const &e) { std::clog << "[eventloop] exception caught " << std::quoted(e.what()) << std::endl; } 
                catch (...)                     { std::clog << "[eventloop] unknown exception caught" << std::endl;                   } 
            }
        });

    tgroup.join_all();
}

使用随机客户端

将睡眠更改为随机的,使其有时有效,有时超时:

std::mt19937 prng { std::random_device{}() };
for (int i = 0; i < 4; i++) {
    ba::write(s, ba::buffer(std::string("A")));
    std::this_thread::sleep_for(std::uniform_int_distribution<>(200, 400)(prng) * 1ms);
}

在我的系统上打印:

1. received: OK
2. received: OK
3. received: OK
canceled (timeout)
4 exception read_some: End of file
5. received: OK
canceled (timeout)
6 exception read_some: End of file
7. received: OK
8. received: OK

看妈妈,没有手

更简单的是,省略特殊情况消息,实际上并没有太大变化:

ba::spawn(strand_, [this, self = shared_from_this()](ba::yield_context yield) {
    try {
        ba::steady_timer timer(strand_, TIMEOUT_LIMIT);
        timer.async_wait([this](error_code ec) {
            if (ba::error::operation_aborted != ec) 
                socket_.close();
            });

        std::string packet;
        ba::async_read(socket_,
                ba::dynamic_buffer(packet),
                ba::transfer_exactly(DATA_LEN_4), yield);

        ba::write(socket_, ba::buffer("OK"s));
    } catch(std::exception const& e) {
        std::clog << "error " << std::quoted(e.what()) << std::endl;
    }
});

请注意,我们甚至不再需要timer_作为成员,并且它的析构函数也会在到达范围末尾时自动正确地取消计时器。

输出实际上并没有太大变化:

1. received: OK
2. received: OK
3. received: OK
error "Operation canceled"
4 exception read_some: End of file
5. received: OK
6. received: OK
7. received: OK
error "Operation canceled"
8 exception read_some: End of file
error "Operation canceled"
9 exception read_some: End of file
于 2019-11-12T00:41:19.407 回答