352 lines
10 KiB
C++
352 lines
10 KiB
C++
/*
|
|
* cmd_satori_chat.cpp
|
|
* Author: Benjamin Sergeant
|
|
* Copyright (c) 2017 Machine Zone. All rights reserved.
|
|
*/
|
|
|
|
#include "IXTest.h"
|
|
#include "catch.hpp"
|
|
#include <chrono>
|
|
#include <iostream>
|
|
#include <ixcobra/IXCobraConnection.h>
|
|
#include <ixcrypto/IXUuid.h>
|
|
#include <ixsnake/IXRedisServer.h>
|
|
#include <ixsnake/IXSnakeServer.h>
|
|
|
|
using namespace ix;
|
|
|
|
namespace
|
|
{
|
|
std::atomic<size_t> incomingBytes(0);
|
|
std::atomic<size_t> outgoingBytes(0);
|
|
|
|
void setupTrafficTrackerCallback()
|
|
{
|
|
ix::CobraConnection::setTrafficTrackerCallback([](size_t size, bool incoming) {
|
|
if (incoming)
|
|
{
|
|
incomingBytes += size;
|
|
}
|
|
else
|
|
{
|
|
outgoingBytes += size;
|
|
}
|
|
});
|
|
}
|
|
|
|
class CobraChat
|
|
{
|
|
public:
|
|
CobraChat(const std::string& user, const std::string& session, const std::string& endpoint);
|
|
|
|
void subscribe(const std::string& channel);
|
|
void start();
|
|
void stop();
|
|
void run();
|
|
bool isReady() const;
|
|
|
|
void sendMessage(const std::string& text);
|
|
size_t getReceivedMessagesCount() const;
|
|
|
|
bool hasPendingMessages() const;
|
|
Json::Value popMessage();
|
|
|
|
private:
|
|
std::string _user;
|
|
std::string _session;
|
|
std::string _endpoint;
|
|
|
|
std::queue<Json::Value> _publish_queue;
|
|
mutable std::mutex _queue_mutex;
|
|
|
|
std::thread _thread;
|
|
std::atomic<bool> _stop;
|
|
|
|
ix::CobraConnection _conn;
|
|
std::atomic<bool> _connectedAndSubscribed;
|
|
|
|
std::queue<Json::Value> _receivedQueue;
|
|
|
|
std::mutex _logMutex;
|
|
};
|
|
|
|
CobraChat::CobraChat(const std::string& user,
|
|
const std::string& session,
|
|
const std::string& endpoint)
|
|
: _user(user)
|
|
, _session(session)
|
|
, _endpoint(endpoint)
|
|
, _stop(false)
|
|
, _connectedAndSubscribed(false)
|
|
{
|
|
}
|
|
|
|
void CobraChat::start()
|
|
{
|
|
_thread = std::thread(&CobraChat::run, this);
|
|
}
|
|
|
|
void CobraChat::stop()
|
|
{
|
|
_stop = true;
|
|
_thread.join();
|
|
}
|
|
|
|
bool CobraChat::isReady() const
|
|
{
|
|
return _connectedAndSubscribed;
|
|
}
|
|
|
|
size_t CobraChat::getReceivedMessagesCount() const
|
|
{
|
|
return _receivedQueue.size();
|
|
}
|
|
|
|
bool CobraChat::hasPendingMessages() const
|
|
{
|
|
std::unique_lock<std::mutex> lock(_queue_mutex);
|
|
return !_publish_queue.empty();
|
|
}
|
|
|
|
Json::Value CobraChat::popMessage()
|
|
{
|
|
std::unique_lock<std::mutex> lock(_queue_mutex);
|
|
auto msg = _publish_queue.front();
|
|
_publish_queue.pop();
|
|
return msg;
|
|
}
|
|
|
|
//
|
|
// Callback to handle received messages, that are printed on the console
|
|
//
|
|
void CobraChat::subscribe(const std::string& channel)
|
|
{
|
|
std::string filter;
|
|
_conn.subscribe(channel, filter, [this](const Json::Value& msg) {
|
|
spdlog::info("receive {}", msg.toStyledString());
|
|
|
|
if (!msg.isObject()) return;
|
|
if (!msg.isMember("user")) return;
|
|
if (!msg.isMember("text")) return;
|
|
if (!msg.isMember("session")) return;
|
|
|
|
std::string msg_user = msg["user"].asString();
|
|
std::string msg_text = msg["text"].asString();
|
|
std::string msg_session = msg["session"].asString();
|
|
|
|
// We are not interested in messages
|
|
// from a different session.
|
|
if (msg_session != _session) return;
|
|
|
|
// We are not interested in our own messages
|
|
if (msg_user == _user) return;
|
|
|
|
_receivedQueue.push(msg);
|
|
|
|
std::stringstream ss;
|
|
ss << std::endl << msg_user << " > " << msg_text << std::endl << _user << " > ";
|
|
log(ss.str());
|
|
});
|
|
}
|
|
|
|
void CobraChat::sendMessage(const std::string& text)
|
|
{
|
|
Json::Value msg;
|
|
msg["user"] = _user;
|
|
msg["session"] = _session;
|
|
msg["text"] = text;
|
|
|
|
std::unique_lock<std::mutex> lock(_queue_mutex);
|
|
_publish_queue.push(msg);
|
|
}
|
|
|
|
//
|
|
// Do satori communication on a background thread, where we can have
|
|
// something like an event loop that publish, poll and receive data
|
|
//
|
|
void CobraChat::run()
|
|
{
|
|
// "chat" conf
|
|
std::string appkey("FC2F10139A2BAc53BB72D9db967b024f");
|
|
std::string channel = _session;
|
|
std::string role = "_sub";
|
|
std::string secret = "66B1dA3ED5fA074EB5AE84Dd8CE3b5ba";
|
|
SocketTLSOptions socketTLSOptions;
|
|
|
|
_conn.configure(appkey,
|
|
_endpoint,
|
|
role,
|
|
secret,
|
|
ix::WebSocketPerMessageDeflateOptions(true),
|
|
socketTLSOptions);
|
|
_conn.connect();
|
|
|
|
_conn.setEventCallback([this, channel](ix::CobraConnectionEventType eventType,
|
|
const std::string& errMsg,
|
|
const ix::WebSocketHttpHeaders& headers,
|
|
const std::string& subscriptionId,
|
|
CobraConnection::MsgId msgId) {
|
|
if (eventType == ix::CobraConnection_EventType_Open)
|
|
{
|
|
log("Subscriber connected: " + _user);
|
|
for (auto&& it : headers)
|
|
{
|
|
log("Headers " + it.first + " " + it.second);
|
|
}
|
|
}
|
|
else if (eventType == ix::CobraConnection_EventType_Authenticated)
|
|
{
|
|
log("Subscriber authenticated: " + _user);
|
|
subscribe(channel);
|
|
}
|
|
else if (eventType == ix::CobraConnection_EventType_Error)
|
|
{
|
|
log(errMsg + _user);
|
|
}
|
|
else if (eventType == ix::CobraConnection_EventType_Closed)
|
|
{
|
|
log("Connection closed: " + _user);
|
|
}
|
|
else if (eventType == ix::CobraConnection_EventType_Subscribed)
|
|
{
|
|
log("Subscription ok: " + _user + " subscription_id " + subscriptionId);
|
|
_connectedAndSubscribed = true;
|
|
}
|
|
else if (eventType == ix::CobraConnection_EventType_UnSubscribed)
|
|
{
|
|
log("Unsubscription ok: " + _user + " subscription_id " + subscriptionId);
|
|
}
|
|
else if (eventType == ix::CobraConnection_EventType_Published)
|
|
{
|
|
Logger() << "Subscriber: published message acked: " << msgId;
|
|
}
|
|
});
|
|
|
|
while (!_stop)
|
|
{
|
|
{
|
|
while (hasPendingMessages())
|
|
{
|
|
auto msg = popMessage();
|
|
|
|
std::string text = msg["text"].asString();
|
|
|
|
std::stringstream ss;
|
|
ss << "Sending msg [" << text << "]";
|
|
log(ss.str());
|
|
|
|
Json::Value channels;
|
|
channels.append(channel);
|
|
_conn.publish(channels, msg);
|
|
}
|
|
}
|
|
|
|
ix::msleep(50);
|
|
}
|
|
|
|
_conn.unsubscribe(channel);
|
|
|
|
ix::msleep(50);
|
|
_conn.disconnect();
|
|
|
|
_conn.setEventCallback([](ix::CobraConnectionEventType /*eventType*/,
|
|
const std::string& /*errMsg*/,
|
|
const ix::WebSocketHttpHeaders& /*headers*/,
|
|
const std::string& /*subscriptionId*/,
|
|
CobraConnection::MsgId /*msgId*/) { ; });
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("Cobra_chat", "[cobra_chat]")
|
|
{
|
|
SECTION("Exchange and count sent/received messages.")
|
|
{
|
|
int port = getFreePort();
|
|
snake::AppConfig appConfig = makeSnakeServerConfig(port);
|
|
|
|
// Start a redis server
|
|
ix::RedisServer redisServer(appConfig.redisPort);
|
|
auto res = redisServer.listen();
|
|
REQUIRE(res.first);
|
|
redisServer.start();
|
|
|
|
// Start a snake server
|
|
snake::SnakeServer snakeServer(appConfig);
|
|
snakeServer.run();
|
|
|
|
int timeout;
|
|
setupTrafficTrackerCallback();
|
|
|
|
std::string session = ix::generateSessionId();
|
|
|
|
std::stringstream ss;
|
|
ss << "ws://localhost:" << port;
|
|
std::string endpoint = ss.str();
|
|
|
|
CobraChat chatA("jean", session, endpoint);
|
|
CobraChat chatB("paul", session, endpoint);
|
|
|
|
chatA.start();
|
|
chatB.start();
|
|
|
|
// Wait for all chat instance to be ready
|
|
timeout = 10 * 1000; // 10s
|
|
while (true)
|
|
{
|
|
if (chatA.isReady() && chatB.isReady()) break;
|
|
ix::msleep(10);
|
|
|
|
timeout -= 10;
|
|
if (timeout <= 0)
|
|
{
|
|
snakeServer.stop();
|
|
redisServer.stop();
|
|
REQUIRE(false); // timeout
|
|
}
|
|
}
|
|
|
|
// Add a bit of extra time, for the subscription to be active
|
|
ix::msleep(1000);
|
|
|
|
chatA.sendMessage("from A1");
|
|
chatA.sendMessage("from A2");
|
|
chatA.sendMessage("from A3");
|
|
|
|
chatB.sendMessage("from B1");
|
|
chatB.sendMessage("from B2");
|
|
|
|
// 1. Wait for all messages to be sent
|
|
timeout = 10 * 1000; // 10s
|
|
while (chatA.hasPendingMessages() || chatB.hasPendingMessages())
|
|
{
|
|
ix::msleep(10);
|
|
|
|
timeout -= 10;
|
|
if (timeout <= 0)
|
|
{
|
|
snakeServer.stop();
|
|
redisServer.stop();
|
|
REQUIRE(false); // timeout
|
|
}
|
|
}
|
|
|
|
// Give us 1s for all messages to be received
|
|
ix::msleep(1000);
|
|
|
|
chatA.stop();
|
|
chatB.stop();
|
|
|
|
REQUIRE(chatA.getReceivedMessagesCount() == 2);
|
|
REQUIRE(chatB.getReceivedMessagesCount() == 3);
|
|
|
|
spdlog::info("Incoming bytes {}", incomingBytes);
|
|
spdlog::info("Outgoing bytes {}", outgoingBytes);
|
|
|
|
spdlog::info("Stopping snake server...");
|
|
snakeServer.stop();
|
|
|
|
spdlog::info("Stopping redis server...");
|
|
redisServer.stop();
|
|
}
|
|
}
|