c++ - 在接受第二个连接后,由于套接字不再存在,导致Boost ASIO段故障

标签 c++ boost-asio

我目前正在尝试使用boost::asio获得一个聊天室类型的程序。在当前状态下,服务器可以接受来自客户端的连接,然后客户端可以向服务器发送消息(此时,服务器对消息进行一点格式化,然后将其发送给当前连接的每个客户端) 。
我遇到的问题如下:

server starts
client 0 connects
client 0 sends a message
  (the message is received by the server and then sent back to client 0 who receives it correctly)
client 1 connects
client 1 sends a message
  (the message is received by the server and then sent back to client 0 and client 1 who both receive it correctly)
client 0 tries to send a message again
  (the message is received by the server and the server processes the header then attempts to call async_read again to read the body of the message, however the socket member variable for client 0 no longer exists and I get a segfault)
我发现这真的很奇怪,因为服务器仍然具有客户端0的有效套接字对象(否则它将无法将客户端1的消息发送到客户端0)。
以下是相关代码:
tcp_connection类(发生段故障的位置)
#include <deque>

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

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

class tcp_connection {
public:
    tcp_connection(tcp::socket socket_, int id, std::function<void (std::size_t, char*, std::size_t)> read_handler) 
      : socket_(std::move(socket)), id_(id), read_handler_(read_handler) {
    }
    
    void start() {
        char first_message[] = "server: connected";
        net_message msg(first_message, strlen(first_message));
        send(msg);
          
        read_header();
    }
    
    void send(net_message msg) {
        bool write_in_progress = !write_messages_.empty();
        write_messages_.push_back(msg);
        if (!write_in_progress) {
            do_write();
        }
    }
    
    int get_id() { return id_; }
    
private:
    void read_header() {
        boost::asio::async_read(socket_, boost::asio::buffer(read_message_.get_data(), net_message::header_length),
          boost::bind(&tcp_connection::handle_read_header, this, boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred));
    }
    
    void handle_read_header(const boost::system::error_code e, std::size_t bytes_transferred) {
        read_message_.decode_header();
        read_body();
    }
    
    void read_body() {
        /*
        ######################
        THIS IS WHERE THE SEGFAULT OCCURS.
        socket_ is no longer valid for some reason
        despite socket_ still being valid for any async_write 
        operations that need to be handled by the do_write() function
        ######################
        */
        boost::asio::async_read(socket_, boost::asio::buffer(read_message_.get_data() + net_message::header_length, read_message_.get_body_length()),
          boost::bind(&tcp_connection::handle_read_body, this, boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred));
    }
    
    void handle_read_body(const boost::system::error_code e, std::size_t bytes_transferred) {
        char body[read_message_.get_body_length()];
        memcpy(body, read_message_.get_body(), read_message_.get_body_length());
        // call the read_handler from the net_server object
        read_handler_(id_, body, read_message_.get_body_length());
        read_header();
    }
    
    void handle_write(const boost::system::error_code e, std::size_t bytes_transferred) {
    }
    
    void do_write() {
        boost::asio::async_write(socket_, boost::asio::buffer(write_messages_.front().get_data(), 
            write_messages_.front().get_body_length() + net_message::header_length),
          [this] (boost::system::error_code ec, std::size_t /*length*/) {
              if (!ec) {
                  write_messages_.pop_front();
                  if (!write_messages_.empty()) {
                      do_write();
                  }
              } else {
                  std::cerr << "error with writing to client " << id_ << " with error code: " << ec << std::endl;
              }
          });
    }
    
    tcp::socket socket_;
    std::function<void (std::size_t, char*, std::size_t)> read_handler_;
    net_message read_message_;
    std::deque<net_message> write_messages_;
    int id_;
};
net_server类
class net_server {
public:
    net_server(boost::asio::io_context& io_context, std::size_t port, 
               std::function<void (std::size_t)> accept_handler,
               std::function<void (std::size_t, char*, std::size_t)> read_handler)
      : io_context_(io_context), acceptor_(io_context, tcp::endpoint(tcp::v4(), 1234)),
        accept_handler_(accept_handler), read_handler_(read_handler)  {
            start_accept();
    }
    
    void send_to(std::size_t id, const char* body, std::size_t length) {
        net_message msg(body, length);
        connections_[id].send(msg);
    }
    
    void send_to_all(const char* body, std::size_t length) {
        net_message msg(body, length);
        for (int i = 0; i < connections_.size(); i++) {
            connections_[i].send(msg);
        }
    }
    
    void send_to_all_except(std::size_t id, const char* body, std::size_t length) {
        net_message msg(body, length);
        for (int i = 0; i < connections_.size(); i++) {
            if (i == id) continue;
            connections_[i].send(msg);
        }
    }
    
private:
    void start_accept() {
        acceptor_.async_accept(
          [this](boost::system::error_code ec, tcp::socket socket) {
            if (!ec) {
                std::unique_lock lock(connections_mutex_);
                std::size_t index = connections_.size();
                connections_.emplace_back(std::move(socket), connections_.size(), read_handler_);
                lock.unlock();

                connections_[index].start();
                accept_handler_(index);
            }
            start_accept();
          });
    }

    boost::asio::io_context& io_context_;
    tcp::acceptor acceptor_;
    
    std::vector<tcp_connection> connections_;
    std::mutex connections_mutex_;
    
    std::function<void (std::size_t)> accept_handler_;
    std::function<void (std::size_t, char*, std::size_t)> read_handler_;
};
设置服务器的主要cpp程序
#include <iostream>

class client {
public:
    client()
      : valid_(false)
    {}
    client(int id)
      : id_(id), valid_(true)
    {}
    
    const char * get_name() const {
        std::string str("Client ");
        str += std::to_string(id_);
        return str.c_str();
    }
private:
    int id_;
    bool valid_;
};

class chat_server {
public:
    chat_server(boost::asio::io_context& io_context, std::size_t port)
      : server_(io_context, port, std::bind(&chat_server::handle_accept, this, std::placeholders::_1),
        std::bind(&chat_server::handle_read, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))
    {}
    
    void handle_accept(std::size_t client_index) {
        std::scoped_lock lock(clients_mutex_);
        if (clients_.size() != client_index) {
            std::cerr << "New client connecting at index " << client_index << 
                    " however, clients_ vector currently has size " << clients_.size() << std::endl;
            if (clients_.size() < client_index) {
                clients_.resize(client_index);
                clients_.emplace_back(client_index);
            } else {
                clients_[client_index] = client(client_index);
            }
        } else {
            clients_.emplace_back(client_index);
        }
        std::cout << "New client with id: " << client_index << std::endl;
    }
    
    void handle_read(std::size_t sender, char* body, std::size_t length) {
        // whenever the server receives a message, this function will be called
        // where clients[sender] will be the connection that sent the message
        // body will be a pointer to the start of the body of the message
        // and length will be the length of the body 
        // we will process the message here and decide if / what to send in response
        // (for example, in a chat server, we'd want to forward the message to every client
        // with the name of the sender attached to it so that clients can update the chat dialogue)
        std::size_t sender_name_len = strlen(clients_[sender].get_name());
        std::size_t new_message_length = sender_name_len + length + 3;
        char new_message[new_message_length];
        
        sprintf(new_message, "%s: ", clients_[sender].get_name());
        memcpy(new_message + sender_name_len + 2, body, length);
        new_message[new_message_length - 1] = '\0';
        
        std::cout << new_message << std::endl;
        
        server_.send_to_all(new_message, new_message_length-1);
    }
private:
    net_server server_;
    std::vector<client> clients_;
    std::mutex clients_mutex_;
};

int main() {
    try {
        boost::asio::io_context io_context;
        chat_server serv(io_context, 1234);
        io_context.run();
    } catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    
    return 0;
}
我要为服务器类维护一个tcp_connections列表,每个列表代表一个已连接到服务器的客户端。当服务器接受连接时,将为该连接创建一个tcp_connection对象,然后该tcp_connection对象将启动一个无限的异步“read_header-> read_body-> repeat”循环。每当服务器从任何客户端接收到消息时,它都应格式化该消息,然后将其发送到列表中的每个tcp_connection。

最佳答案

向其添加新元素时,将重新分配connections_成员变量。在tcp_connection中的各种处理程序中,您正在捕获this,当重新分配 vector 时,this的值将更改,然后您的处理程序将尝试对对象的旧副本进行操作,从而导致未定义的行为。
简单的解决方案是使connections_ vector 成为std::shared_ptr vector 。
最好的做法是在处理程序中捕获对象的shared_ptr,以使对象在执行回调之前不会超出范围。例如。:

void do_write() {
    auto self = shared_from_this();
    boost::asio::async_write(socket_, boost::asio::buffer(write_messages_.front().get_data(), 
        write_messages_.front().get_body_length() + net_message::header_length),
      [self, this] (boost::system::error_code ec, std::size_t /*length*/) {
          if (!ec) {
              write_messages_.pop_front();
              if (!write_messages_.empty()) {
                  do_write();
              }
          } else {
              std::cerr << "error with writing to client " << id_ << " with error code: " << ec << std::endl;
          }
      });
}
您需要从tcp_connection派生std::shared_from_this<tcp_connection>并确保在设置任何处理程序之前已创建shared_ptr(例如,不要在构造函数中创建处理程序)。

关于c++ - 在接受第二个连接后,由于套接字不再存在,导致Boost ASIO段故障,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63334208/

相关文章:

c++ - Boost.Asio - 不执行所有处理程序

c++ - 提升asio以便同步服务器保持TCP session 打开(使用Google Proto缓冲区)

c++ - 从 C++ 程序运行 shell 脚本会自动将 shell 脚本的输出显示到控制台吗?

c++ - 在提升图中使用特征类型

c++ - 扩展类以调试 : public API, 隐藏实现或其他?

c++ - Boost - 在服务器关闭套接字上为新客户端 fork

c++ - boost::asio::io_service 就绪处理程序的定义是什么?

c++ - Winsock 通过 recv() 接收随机字母

c++ - 在 QDialog::exec() 期间使用 QThreads 执行 QWidget::grab()

c++ - strand.post 和 strand.wrap 的性能区别是什么?