简体   繁体   English

我应该如何在 boost::asio 的客户端应用程序中同时使用 async_read_until 和 async_write?

[英]How should I use async_read_until and async_write simultaneously in the client app in boost::asio?

My motivation.我的动力。

I am trying to build a simple messenger.我正在尝试构建一个简单的信使。 Currently I have written client and server apps that support "mail like functional", that is they lack chat interaction that you have in every instant messenger.目前,我已经编写了支持“类似功能的邮件”的客户端和服务器应用程序,即它们缺少您在每个即时消息中拥有的聊天交互。

Here is a model I use.这是我使用的 model。

Server: The server for every connected client has a dedicated Service class that provides actual service.服务器:每个连接的客户端的服务器都有一个提供实际服务的专用Service class。 An instance of the Service class has an id. Service class 的实例有一个 id。

Client : At particular moment simultaneously starts reading messages from and writing messages to the associated Service instance. Client :在特定时刻同时开始从关联的 Service 实例读取消息并将消息写入关联的Service实例。

Tracker : Records current sessions of users by saving their logins and Service ids in a map.跟踪器:通过将用户的登录名和Service ID 保存在 map 中来记录用户的当前会话。 Also records opened chats by saving key-value pairs (chat participant id 1, chat participant id 2).还通过保存键值对(聊天参与者 id 1,聊天参与者 id 2)记录打开的聊天。 I use logins and ids of users interchangeably because I have a database.我可以互换使用用户的登录名和 ID,因为我有一个数据库。

Here is a typical usage scenario.这是一个典型的使用场景。

  1. A user is trying to log in. The server dedicates to the user the Service instance with id 1. Then the user identified as Bob.一个用户正在尝试登录。服务器将 ID 为 1 的Service实例专用于该用户。然后将用户标识为 Bob。
  2. Bob opens a chat with Ann. Bob 开始与 Ann 聊天。 Tracker records that Bob uses Service 1 and that Bob opened the chat with Ann. Tracker记录 Bob 使用Service 1 以及 Bob 打开了与 Ann 的聊天。
  3. A user is trying to log in. The server dedicates to the user the Service instance with id 2. Then the user identified as Ann.用户正在尝试登录。服务器将 ID 为 2 的Service实例专用于用户。然后用户标识为 Ann。
  4. Ann opens a chat with Bob. Ann 开始与 Bob 聊天。 Tracker records that Ann uses Service 2 and that Ann opened the chat with Bob. Tracker记录了 Ann 使用Service 2 以及 Ann 打开了与 Bob 的聊天。
  5. Ann writes a message to Bob. Ann 给 Bob 写了一条消息。 Service 2 receives the message and asks Service 1 to send the message to Bob's chat if Bob has opened the chat with Ann.如果 Bob 已打开与 Ann 的聊天, Service 2 会收到消息并要求Service 1 将消息发送到 Bob 的聊天。 For that purpose I use Tracker .为此,我使用Tracker In our case Bob is in the chat so Bob's client app should read the message from Service 1. Otherwise Service 2 only stores the new message in the database.在我们的例子中,Bob 正在聊天中,所以 Bob 的客户端应用程序应该从Service 1 读取消息。否则, Service 2 只会将新消息存储在数据库中。

When a user opens a chat with somebody the client app simultaneously starts reading and writing messages to the associate Service instance.当用户打开与某人的聊天时,客户端应用程序同时开始向关联的Service实例读取和写入消息。

Issue问题

  1. Bob opens a chat with Ann. Bob 开始与 Ann 聊天。 Ann opens a chat with Bob. Ann 开始与 Bob 聊天。
  2. Ann sends messages.安发消息。 They are displayed in Bobs chat.它们显示在 Bobs 聊天中。
  3. Bob sends a message.鲍勃发送消息。 It is not displayed in Ann's chat.它不会显示在 Ann 的聊天中。 Moreover, further Ann's messages are no longer displayed in Bob's chat.此外,Ann 的其他消息不再显示在 Bob 的聊天中。

Here is a portion of my server code.这是我的服务器代码的一部分。 I have added some context but you probably want to look at Service::onMessageReceived , Service::receive_message , Service::send_to_chat我添加了一些上下文,但您可能想查看Service::onMessageReceivedService::receive_messageService::send_to_chat

/// Struct to track active sessions of clients
struct Tracker {
  static std::mutex current_sessions_guard; ///< mutex to lock the map of current sessions between threads
  static std::map<long, long> current_sessions; 
  static std::map<long, int> client_to_service_id;
};

Class that provides the actual service in the client-service model Class 在客户端服务 model 中提供实际服务

class Service {
public:
  void send_to_chat(const std::string& new_message) {
    asio::async_write(*m_sock.get(), asio::buffer(new_message),
      [this]() {
        onAnotherPartyMessageSent(); 
      });
  } 

private:
  void onReceivedReady();
  void receive_message() {
    /// Server loop for reading messages from the client
    spdlog::info("[{}] in receive_message", service_id);

    asio::async_read_until(*m_sock.get(), *(m_message.get()), '\n',
      [this]() {
        onMessageReceived();
      });
  } 
  void onMessageReceived();

private:
  std::shared_ptr<asio::ip::tcp::socket> m_sock; ///< Pointer to an active socket that is used to communicate
                                                 ///< with the client
  int service_id;  
  long dialog_id = -1, client_id = -1, another_party_id = -1;
  std::shared_ptr<asio::streambuf> m_message;
};

Definitions of methods方法的定义


void Service::onMessageReceived() {
  /// Updates the database with the new message and asks Service instance of another participant
  /// to send the message if they opened this chat.

  std::istream istrm(m_message.get());
  std::string new_message;
  std::getline(istrm, new_message);
  m_message.reset(new asio::streambuf);

  std::unique_lock<std::mutex> tracker_lock(Tracker::current_sessions_guard);

  if (Tracker::current_sessions.find(another_party_id) != Tracker::current_sessions.end()) {
    if (Tracker::current_sessions[another_party_id] == client_id) {
      int another_party_service_id = Tracker::client_to_service_id[another_party_id];
      std::string formatted_msg = _form_message_str(login, new_message);
      
      spdlog::info("[{}] sends to chat '{}'", another_party_service_id, new_message);

      Server::launched_services[another_party_service_id]->send_to_chat(formatted_msg);
    }
  }
  tracker_lock.unlock();
  receive_message();
} 

Here is a portion of my client code.这是我的客户端代码的一部分。 I have added some context but you probably want to look at AsyncTCPClient::onSentReady , AsyncTCPClient::message_send_loop , AsyncTCPClient::message_wait_loop .我添加了一些上下文,但您可能想查看AsyncTCPClient::onSentReadyAsyncTCPClient::message_send_loopAsyncTCPClient::message_wait_loop

/// Struct that stores a session with the given server
struct Session {
  asio::ip::tcp::socket m_sock; //!< The socket for the client application to connect to the server
  asio::ip::tcp::endpoint m_ep; //!< The server's endpoint
  std::string current_chat;

  std::shared_ptr<asio::streambuf> m_chat_buf;
  std::shared_ptr<asio::streambuf> m_received_message;
};

/// Class that implements an asynchronous TCP client to interact with Service class
class AsyncTCPClient: public asio::noncopyable {

  void onSentReady(std::shared_ptr<Session> session) {
  
    msg_wait_thread.reset(new std::thread([this, session] {
      asio::async_read_until(session->m_sock, *(session->m_received_message.get()), "\n", 
        [this, session] () {
          message_wait_loop(session);
        });
      }));
    msg_wait_thread->detach();

    msg_thread.reset(new std::thread([this, session] {
      message_send_loop(session);
      }));

    msg_thread->detach();
  } 


  void message_send_loop(std::shared_ptr<Session> session) {
    /// Starts loop in the current chat enabling the client to keep sending messages to another party
    logger->info("'{}' in message_send_loop", session->login);

    clear_console();
    m_console.write(session->current_chat);
    m_console.write("Write your message: ");

    std::string new_message;

    // We use a do/while loop to prevent empty messages either because of the client input
    // or \n's that were not read before

    do {
      new_message = m_console.read();
    } while (new_message.empty());
    

    std::unique_lock<std::mutex> lock_std_out(std_out_guard);
    session->current_chat.append(_form_message_str(session->login, new_message));
    lock_std_out.unlock();

    asio::async_write(session->m_sock, asio::buffer(new_message + "\n"), 
      [this, session] () {
        message_send_loop(session);
      }); 
  } 

  void message_wait_loop(std::shared_ptr<Session> session) {
    /// Starts loop in the current chat enabling the client to keep reading messages from another party

    logger->info("'{}' in message_wait_loop", session->login);

    std::istream istrm(session->m_received_message.get());
    std::string received_message;
    std::getline(istrm, received_message);

    session->m_received_message.reset(new asio::streambuf);

    std::unique_lock<std::mutex> lock_std_out(std_out_wait_guard);
    session->current_chat.append(received_message + "\n");
    lock_std_out.unlock();

    clear_console();
    m_console.write(session->current_chat);
    m_console.write("Write your message: ");
    
    asio::async_read_until(session->m_sock, *(session->m_received_message.get()), "\n", 
      [this, session] (std::size_t) {
        message_wait_loop(session);
      });
  }

private:
  asio::io_context m_ios;
};

So, when I described the issue I do not have "'{}' in message_wait_loop" logs for both clients at the point 3).因此,当我描述该问题时,我在第 3 点的两个客户端都没有"'{}' in message_wait_loop"日志)。 But I have these logs at the point 2) for the Bob's client.但是我在第 2 点为 Bob 的客户提供了这些日志。

Also I use Console from answer here .我也在这里使用答案中的控制台。 It removes echo and controls standard input/output resources by means of mutexes.它通过互斥体去除回声并控制标准输入/输出资源。 However it does not solve my problem.但是它并不能解决我的问题。

Any help would be appreciated.任何帮助,将不胜感激。

There's too much code and too little.代码太多而太少。 Too much for the question, and too little to actually suggest improvements.这个问题太多了,而实际上建议改进的太少了。 I see an overuse of shared_ptr, threads, In particular it is very weird to run async-operations on their own threads.我看到过度使用 shared_ptr,线程,特别是在他们自己的线程上运行异步操作非常奇怪。 Let alone detached:更别说分离了:

msg_wait_thread.reset(new std::thread([this, session] {
      asio::async_read_until(session->m_sock, *(session->m_received_message.get()), "\n", 
        [this, session] () {
          message_wait_loop(session);
        });
      }));
    msg_wait_thread->detach();

That whole thing is best replaced by the completely equivalent (but safer)整个事情最好用完全等效的(但更安全)代替

  asio::async_read_until(session->m_sock, *(session->m_received_message.get()), "\n", 
    [this, session] () {
      message_wait_loop(session);
    });

I imagine the read loop is on a thread so that the input wouldn't block.我想读循环在一个线程上,这样输入就不会阻塞。 However, it becomes much easier if you consider the main thread the "UI thread" (it is), and accept that console IO is blocking there, instead posting the resulting requests to a single IO thread for all the non-blocking operations.但是,如果您将主线程视为“UI 线程”(确实如此)并接受控制台 IO 在那里阻塞,而不是将结果请求发布到单个 IO 线程以进行所有非阻塞操作,则会变得容易得多。

If you share a link to a repo or something I'm happy to look at it more.如果您分享指向回购或其他内容的链接,我很乐意多看它。

UPDATE更新

In the comments I reviewed the code from the github repo and posted a PR: https://github.com/cepessh/mymsg/pull/1在评论中,我查看了 github 存储库中的代码并发布了 PR: https://github.com/cepessh/mymsg/pull/1

This is a very raw proof-of-concept.这是一个非常原始的概念验证。 I have included many changes that aren't actually related to the suggested concurrency fix, but they happened:我已经包含了许多实际上与建议的并发修复无关的更改,但它们发生了:

  • to allow me to run让我跑
  • during review (you will probably want to look at a number of those changes and keep them anyways)在审查期间(您可能希望查看其中的一些更改并保留它们)
  • fixes that were apparently missing from main branch (eg the default value for Message.read_by_recipient database column) main分支中明显缺失的修复(例如Message.read_by_recipient数据库列的默认值)

You should be able to work out what changes were made and why by the commit messages .您应该能够通过提交消息了解进行了哪些更改以及更改的原因。

Only the last two commits actually focus on the idea discussed in chat .只有最后两个提交真正关注聊天中讨论的想法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM