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.
Server: The server for every connected client has a dedicated Service class that provides actual service. An instance of the Service class has an id.
Client: At particular moment simultaneously starts reading messages from and writing messages to the associated Service instance.
Tracker: Records current sessions of users by saving their logins and Service ids in a map. Also records opened chats by saving key-value pairs (chat participant id 1, chat participant id 2). I use logins and ids of users interchangeably because I have a database.
Here is a typical usage scenario.
- A user is trying to log in. The server dedicates to the user the
Serviceinstance with id 1. Then the user identified as Bob. - Bob opens a chat with Ann.
Trackerrecords that Bob usesService1 and that Bob opened the chat with Ann. - A user is trying to log in. The server dedicates to the user the
Serviceinstance with id 2. Then the user identified as Ann. - Ann opens a chat with Bob.
Trackerrecords that Ann usesService2 and that Ann opened the chat with Bob. - Ann writes a message to Bob.
Service2 receives the message and asksService1 to send the message to Bob's chat if Bob has opened the chat with Ann. For that purpose I useTracker. In our case Bob is in the chat so Bob's client app should read the message fromService1. OtherwiseService2 only stores the new message in the database.
When a user opens a chat with somebody the client app simultaneously starts reading and writing messages to the associate Service instance.
Issue
- Bob opens a chat with Ann. Ann opens a chat with Bob.
- Ann sends messages. They are displayed in Bobs chat.
- Bob sends a message. It is not displayed in Ann's chat. Moreover, further Ann's messages are no longer displayed in Bob's chat.
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
/// 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 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.
/// 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). But I have these logs at the point 2) for the Bob's client.
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.