diff --git a/CMakeLists.txt b/CMakeLists.txt index d2919ae..dd14aa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,16 @@ -project(DeepPic CXX) - -cmake_minimum_required(VERSION 3.6) +cmake_minimum_required(VERSION 3.20) +project(boost_asio_async_server) set(CMAKE_CXX_STANDARD 20) -file(GLOB_RECURSE HEADER_FILES project/include/*.h) -file(GLOB_RECURSE SOURCE_FILES project/src/*.cpp) +find_package(Boost 1.40.0 REQUIRED system) +add_definitions(-DBOOST_LOG_DYN_LINK) +find_package(Boost COMPONENTS log log_setup REQUIRED) +find_package(nlohmann_json 3.2.0 REQUIRED) +link_directories(${Boost_LIBRARY_DIR}) + +include_directories(project/include ${Boost_INCLUDE_DIRS}) +aux_source_directory(project/src SRC) +add_executable(boost_asio_async_server ${SRC}) -add_executable(main ${HEADER_FILES} ${SOURCE_FILES}) \ No newline at end of file +target_link_libraries(boost_asio_async_server boost_thread pthread ${BOOST_LIBRARIES} nlohmann_json::nlohmann_json Boost::log Boost::log_setup) diff --git a/about_json_api.md b/about_json_api.md new file mode 100644 index 0000000..19abcbe --- /dev/null +++ b/about_json_api.md @@ -0,0 +1,93 @@ +# Всевозможные сценарии комманд + +Клиент, чтобы расшарить документ: +```json +{ + "target": "sharing_document" +} +``` + +Сервер в ответ: +```json +{ + "status": "OK", // или "FAIL" + "target": "sharing_document" // для указания цели, на которую пришел ответ + "auth_token": "dummy_aboba", + "address": "127.0.0.1", + "port": 5555 +} +``` + +
+ +Клиент, чтобы подключиться к документу: +```json +{ + "target": "auth", + "auth_token": "dummy_aboba", + "address": "127.0.0.1", + "port": 5555 +} +``` + +Сервер в ответ: +```json +{ + "status": "OK" // или FAIL + "target": "auth" +} +``` + +
+ +Клиент, чтобы запросить после подключения копию документа +```json +{ + "target": "get_document" +} +``` +Сервер клиенту, у которого есть копия документа: +```json +{ + "target": "get_document" +} +``` +Клиент в ответ на запрос сервера на документ: +```json +{ + "status": "OK", // или FAIL, тогда поле document не обязательно + "target": "get_document", + "document": "some document code" +} +``` + +Сервер изначальному клиенту, который запрашивал документ: +```json +{ + "status": "OK", // или FAIL, тогда поле document не обязательно + "target": "get_document", + "document": "some document code" +} +``` + +
+ +Клиент, чтобы расшарить команду: +```json +{ + "target": "sharing_command", + "command": "some command code" +} +``` + +Сервер в ответ: решительное ничего + +Сервер всем остальным клиентам, подключенным к документу: +```json +{ + "target": "sharing_command", + "command": "some command code" +} +``` + +Клиенты в ответ после получения команды: решительное ничего diff --git a/project/ServerConnection/include/ServerConnection.h b/project/ServerConnection/include/ServerConnection.h new file mode 100644 index 0000000..6b0da1d --- /dev/null +++ b/project/ServerConnection/include/ServerConnection.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#define BUFFER_LENGTH 1024 +#define END_STR "\r\n" + + +/* + * Структура с колбеками, которые будут вызываться после успешного чтения и записи соответственно. + */ +struct ServerConnectionCallbacks { + std::function onReadCb; + std::function onWriteCb; +}; + +class ServerConnection { +public: + ServerConnection(std::string &&url, int port, ServerConnectionCallbacks &&callbacks); + + /* + * Выполняет подключение к серверу и начинает бесконечный цикл чтения сообщений с сервера. + * После того как прочитали сообщение, вызовется onReadCb из ServerConnectionCallbacks. + */ + void start(); + + /** + * Пишет серверу какое-либо сообщение. После записи вызовет onWriteCb, переданный в структуре ServerConnectionCallbacks + * @param message само сообщение + */ + void write(std::string &&message); + +private: + void connectionToServer(); + + void read(); + + void readHandler(boost::system::error_code &err, size_t bytes_transferred); + + std::size_t checkEndOfRead(const boost::system::error_code &err, std::size_t bytes_transferred); + + void writeHandler(boost::system::error_code &err); + + void run_ioc(); + + std::string serverUrl_; + int serverPort_; + + boost::asio::io_context service_; + boost::asio::ip::tcp::socket socket_; + ServerConnectionCallbacks callbacks_; + + std::mutex writeMutex_; + std::condition_variable writeCv_; + std::atomic canWrite_ = true; + + char readBuf_[BUFFER_LENGTH]; + char sendBuf_[BUFFER_LENGTH]; +}; + diff --git a/project/ServerConnection/src/ServerConnection.cpp b/project/ServerConnection/src/ServerConnection.cpp new file mode 100644 index 0000000..9caa738 --- /dev/null +++ b/project/ServerConnection/src/ServerConnection.cpp @@ -0,0 +1,80 @@ +#include + +#include "../include/ServerConnection.h" + +ServerConnection::ServerConnection(std::string &&url, int port, ServerConnectionCallbacks &&callbacks) : socket_(service_), + serverUrl_(std::move(url)), + serverPort_(port), + callbacks_(std::move(callbacks)) { +} + +void ServerConnection::connectionToServer() { + boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(serverUrl_), serverPort_); + socket_.connect(ep); + read(); +} + +void ServerConnection::read() { + boost::asio::async_read(socket_, boost::asio::buffer(readBuf_), + [this](boost::system::error_code err, std::size_t bytes_transferred) -> size_t { + return this->checkEndOfRead(err, bytes_transferred); + }, [this](boost::system::error_code err, std::size_t bytes_transferred) { + this->readHandler(err, bytes_transferred); + }); +} + +void ServerConnection::readHandler(boost::system::error_code &err, size_t bytes_transferred) { + if (err) { + return; + } + + if (callbacks_.onReadCb) { + callbacks_.onReadCb(std::string(readBuf_, readBuf_ + bytes_transferred)); + } + + read(); +} + +void ServerConnection::run_ioc() { + service_.run(); +} + +void ServerConnection::start() { + connectionToServer(); + run_ioc(); +} + +void ServerConnection::write(std::string &&message) { + std::unique_lock ulock(writeMutex_); + writeCv_.wait(ulock, [this]() { return bool(canWrite_); }); + canWrite_ = false; + + for (int i = 0; i < message.length(); ++i) { + sendBuf_[i] = message[i]; + } + + boost::asio::async_write(socket_, boost::asio::buffer(sendBuf_, message.length()), [this](boost::system::error_code err) { + this->writeHandler(err); + }); +} + +void ServerConnection::writeHandler(boost::system::error_code &err) { + if (err) { + return; + } + + if (callbacks_.onWriteCb) { + callbacks_.onWriteCb(err); + } + + canWrite_ = true; + writeCv_.notify_one(); +} + +std::size_t ServerConnection::checkEndOfRead(const boost::system::error_code &err, std::size_t bytes_transferred) { + if (bytes_transferred > 0 && + std::string(readBuf_ + bytes_transferred - 1 - std::strlen(END_STR), readBuf_ + bytes_transferred - 1) == std::string(END_STR)) { + return 0; + } + return 1; +} diff --git a/project/include/Command.h b/project/include/Command.h new file mode 100644 index 0000000..40624e1 --- /dev/null +++ b/project/include/Command.h @@ -0,0 +1,101 @@ +#pragma once + +#include + +#include "Settings.h" +#include "SharedDocumentServer.h" +#include "Connection.h" + +using json = nlohmann::json; + +typedef enum { + SHARING_COMMAND = 0x01, + GET_DOCUMENT = 0x02, + CREATE_DOCUMENT = 0x04 +} command_t; + +class Command { +public: + Command(command_t type_command, std::vector> *connection = nullptr); + + Command(const Command &) = delete; + + Command &operator=(Command &) = delete; + + virtual bool do_command(json &command, std::shared_ptr author); + + ~Command(); + +protected: + Command() : letter_(nullptr) {} + +private: + Command *letter_; +}; + +class GetDocument : public Command { +public: + virtual bool do_command(json &command, std::shared_ptr author) override; + + GetDocument(const GetDocument &) = delete; + + GetDocument &operator=(GetDocument &) = delete; + + ~GetDocument() = default; + +private: + friend class Command; + + explicit GetDocument(std::vector> *connections); + + void getDocumentFromClient(const std::shared_ptr& author); + + void handleGetDocumentFromClient(std::shared_ptr &connection, const std::shared_ptr& author); + + void sendDocumentToNewClients(std::string &&document); + + std::vector> clientsToGetDocument_; + + std::vector> *connections_ = nullptr; +}; + +class SharingCommand : public Command { +public: + virtual bool do_command(json &command, std::shared_ptr author) override; + + SharingCommand(const SharingCommand &) = delete; + + SharingCommand &operator=(SharingCommand &) = delete; + + ~SharingCommand() = default; + +private: + friend class Command; + + explicit SharingCommand(std::vector> *connections); + + std::vector> *connections_ = nullptr; +}; + +class CreateNewDocumentCommand : public Command { +public: + virtual bool do_command(json &command, std::shared_ptr author) override; + + ~CreateNewDocumentCommand(); + +private: + friend class Command; + + CreateNewDocumentCommand() = default; + + std::vector> sharedDocuments_; +}; + +class DocumentCommandBus { +public: + explicit DocumentCommandBus(std::vector> *connection); + bool do_command(std::string &&command, std::shared_ptr author); +private: + Command sharingCommand_; + Command getDocumentCommand_; +}; \ No newline at end of file diff --git a/project/include/CommandConstructor.h b/project/include/CommandConstructor.h new file mode 100644 index 0000000..fd2f7b2 --- /dev/null +++ b/project/include/CommandConstructor.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +using json = nlohmann::json; + +class CommandConstructor { +public: + static json sharingDocumentClient(); + + static json sharingDocumentServer(std::string &&status, std::string &&auth_token, std::string &&address, int port); + + static json authClient(std::string &&auth_token); + + static json authServer(std::string &&status, std::string &&token, std::string &&address, int port); + + static json getDocumentClient(); + + static json getDocumentServer(std::string &&status, std::string &&document); + + static json sharingCommandClient(std::string &&command_); +}; \ No newline at end of file diff --git a/project/include/Connection.h b/project/include/Connection.h new file mode 100644 index 0000000..f57e153 --- /dev/null +++ b/project/include/Connection.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Settings.h" + +class Connection : public std::enable_shared_from_this { +public: + Connection(boost::asio::io_context &service, + std::function, std::string &&)> on_read_cb, + std::function)> on_delete_cb = {}); + + boost::asio::ip::tcp::socket &getSock(); + + void afterConnect(); + + void write(std::string &command, std::function)> onWriteCb = {}); + + void stop(); + + void setAuth(bool value); + + bool getAuth(); + + ~Connection(); + +private: + void read(); + + void readHandler(const boost::system::error_code &err, std::size_t bytes_transferred); + + void writeHandler(const boost::system::error_code &err, std::size_t bytes_transferred, + std::function)> onWriteCb); + + boost::asio::ip::tcp::socket sock_; + + std::function, std::string &&)> onReadCb_; + std::function)> onDeleteCb_; + + boost::beast::flat_buffer buffer_{BUFFER_LENGTH}; + + boost::beast::http::request request_; + boost::beast::http::response response_; + + std::mutex writeMutex_; + std::condition_variable writeCv_; + std::atomic canWrite_ = true; + + bool isAuth_ = false; +}; \ No newline at end of file diff --git a/project/include/MainServer.h b/project/include/MainServer.h new file mode 100644 index 0000000..ed9f8a1 --- /dev/null +++ b/project/include/MainServer.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Server.h" +#include "Connection.h" +#include "Command.h" + +class MainServer { +public: + MainServer(int port, int countThreads); + void runServer(); + +private: + void handleAcceptConnection(std::shared_ptr connection); + + void onReadCb(std::shared_ptr, std::string &&command); + + boost::asio::io_context service_; + Server server_; + Command *createNewDocumentCommand_; + int countThreads_; +}; \ No newline at end of file diff --git a/project/include/Server.h b/project/include/Server.h new file mode 100644 index 0000000..cf3b860 --- /dev/null +++ b/project/include/Server.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "Connection.h" +#include "Settings.h" + +struct ServerCallbacks { + std::function)> onAcceptCb_; + std::function, std::string &&)> onReadCb_; + std::function)> onDeleteCb_; +}; + +class Server { +public: + Server(int port, boost::asio::io_context &service, ServerCallbacks &&callbacks); + + void runServer(); + + int getPort(); + +private: + void startAcceptConnections(); + + void handleAcceptConnection(std::shared_ptr connection, const boost::system::error_code &err); + + boost::asio::ip::tcp::acceptor acceptor_; + int port_; + + ServerCallbacks callbacks_; + + char readBuf_[BUFFER_LENGTH]; + char sendBuf_[BUFFER_LENGTH]; +}; \ No newline at end of file diff --git a/project/include/Settings.h b/project/include/Settings.h new file mode 100644 index 0000000..b4a14a0 --- /dev/null +++ b/project/include/Settings.h @@ -0,0 +1,3 @@ +#pragma once + +constexpr int BUFFER_LENGTH = 10000; \ No newline at end of file diff --git a/project/include/SharedDocumentServer.h b/project/include/SharedDocumentServer.h new file mode 100644 index 0000000..86981e0 --- /dev/null +++ b/project/include/SharedDocumentServer.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include "Server.h" +#include "Connection.h" +#include "Settings.h" + +class DocumentCommandBus; + +class SharedDocumentServer { +public: + static int start_since_port; // начиная с какого порта начнут создаваться объекты SharedDocumentServer + + SharedDocumentServer(boost::asio::io_context &service); + + void startShared(); + + int getPort(); + + std::string getAuthToken(); + + ~SharedDocumentServer(); + +private: + void generateAuthToken(); + + void handleAcceptConnection(std::shared_ptr connection); + + void onDeleteConnection(std::shared_ptr connection); + + bool checkAuthToken(std::string &&token); + + void onReadCb(std::shared_ptr author, std::string &&command); + + + Server server_; + std::string authToken_; + std::vector> connections_; + + DocumentCommandBus *documentCommandBus_; +}; diff --git a/project/src/Command.cpp b/project/src/Command.cpp new file mode 100644 index 0000000..880b310 --- /dev/null +++ b/project/src/Command.cpp @@ -0,0 +1,139 @@ +#include +#include +#include + +#include "Command.h" +#include "CommandConstructor.h" + +using json = nlohmann::json; + +Command::Command(command_t type_command, std::vector> *connections) { + switch (type_command) { + case GET_DOCUMENT: + letter_ = new GetDocument(connections); + break; + case SHARING_COMMAND: + letter_ = new SharingCommand(connections); + break; + case CREATE_DOCUMENT: + letter_ = new CreateNewDocumentCommand(); + break; + } +} + +bool Command::do_command(json &command, std::shared_ptr author) { + letter_->do_command(command, author); + + return true; +} + +Command::~Command() { + delete letter_; +} + +bool GetDocument::do_command(json &command, std::shared_ptr author) { + if (command["target"] == "get_document") { + if (command.contains("status") && command["status"] == "OK") { // если пришел документ от одного из пользователей + sendDocumentToNewClients(std::move(command["document"])); // рассылаем его всем нуждающимся + return true; + } else if (!command.contains("status")) { // если пришел запрос на получение документа + clientsToGetDocument_.push_back(author); // добавляем автора запроса в вектор тех, кому нужен документ + getDocumentFromClient(author); // и запрашиваем документ у одного из участников + return true; + } + } + + return false; +} + +void GetDocument::getDocumentFromClient(const std::shared_ptr& author) { + std::string command_dump = CommandConstructor::getDocumentClient().dump(); + if (!connections_->empty()) { + auto it = connections_->begin(); + while (*it == author) { + it++; + } + if (it != connections_->end()) { + (*(connections_->begin()))->write(command_dump, + [this, author](std::shared_ptr connection) { + this->handleGetDocumentFromClient(connection, author); + }); + } + } +} + +void GetDocument::handleGetDocumentFromClient(std::shared_ptr &connection, const std::shared_ptr& author) { + if (connection == nullptr) { + getDocumentFromClient(author); + } +} + +void GetDocument::sendDocumentToNewClients(std::string &&document) { + std::string command_dump = CommandConstructor::getDocumentServer("OK", std::move(document)).dump(); + for (auto &client: clientsToGetDocument_) { + client->write(command_dump); + } + clientsToGetDocument_.clear(); +} + +GetDocument::GetDocument(std::vector> *connections) { + connections_ = connections; +} + +SharingCommand::SharingCommand(std::vector> *connections) { + connections_ = connections; +} + +bool SharingCommand::do_command(json &command, std::shared_ptr author) { + for (auto &connection: *connections_) { + if (author != connection) { + std::cerr << "SharingCommand::do_command()" << std::endl; + std::string command_dump = command.dump(); + connection->write(command_dump); + } + } + + return true; +} + +bool CreateNewDocumentCommand::do_command(json &command, std::shared_ptr author) { + BOOST_LOG_TRIVIAL(info) << "Create new document"; + + std::shared_ptr shared_document( + new SharedDocumentServer(static_cast(author->getSock().get_executor().context()))); + sharedDocuments_.push_back(shared_document); + shared_document->startShared(); + + std::string command_dump = CommandConstructor::sharingDocumentServer("OK", shared_document->getAuthToken(), "127.0.0.1", + shared_document->getPort()).dump(); + author->write(command_dump); + + return true; +} + +CreateNewDocumentCommand::~CreateNewDocumentCommand() { +} + +DocumentCommandBus::DocumentCommandBus(std::vector> *connection) : getDocumentCommand_(GET_DOCUMENT, + connection), + sharingCommand_(SHARING_COMMAND, + connection) { + +} + +bool DocumentCommandBus::do_command(std::string &&command, std::shared_ptr author) { + try { + auto command_json = json::parse(command); + if (command_json.contains("target")) { + if (command_json["target"] == "sharing_command" && command_json.contains("command")) { + return sharingCommand_.do_command(command_json, author); + } else if (command_json["target"] == "get_document") { + return getDocumentCommand_.do_command(command_json, author); + } + } + return false; + } catch (...) { + return false; + } + +} diff --git a/project/src/CommandConstructor.cpp b/project/src/CommandConstructor.cpp new file mode 100644 index 0000000..ae38d02 --- /dev/null +++ b/project/src/CommandConstructor.cpp @@ -0,0 +1,54 @@ +#include "CommandConstructor.h" + +json CommandConstructor::sharingDocumentClient() { + static json command = {{"target", "sharing_document"}}; + return command; +} + +json CommandConstructor::sharingDocumentServer(std::string &&status, std::string &&auth_token, std::string &&address, int port) { + json command = {{"status", status}, + {"target", "sharing_document"}, + {"auth_token", auth_token}, + {"address", address}, + {"port", port}}; + + return command; +} + +json CommandConstructor::authClient(std::string &&auth_token) { + json command = {{"target", "auth"}, + {"auth_token", auth_token}}; + + return command; +} + +json CommandConstructor::authServer(std::string &&status, std::string &&token, std::string &&address, int port) { + json command = {{"status", status}, + {"target", "auth"}, + {"auth_token", token}, + {"port", port}, + {"address", address}}; + + return command; +} + +json CommandConstructor::getDocumentClient() { + static json command = {{"target", "get_document"}}; + + return command; +} + +json CommandConstructor::getDocumentServer(std::string &&status, std::string &&document) { + json command = {{"status", status}, + {"target", "get_document"}, + {"document", document}}; + + return command; +} + +json CommandConstructor::sharingCommandClient(std::string &&command_) { + json command = {{"target", "sharing_command"}, + {"command", command_}}; + + return command; +} diff --git a/project/src/Connection.cpp b/project/src/Connection.cpp new file mode 100644 index 0000000..b5270fd --- /dev/null +++ b/project/src/Connection.cpp @@ -0,0 +1,105 @@ +#include +#include +#include +#include + +#include "Connection.h" + +Connection::Connection(boost::asio::io_context &service, std::function, std::string &&)> on_read_cb, + std::function)> on_delete_cb) : sock_(service), + onReadCb_(std::move(on_read_cb)), + onDeleteCb_(std::move(on_delete_cb)) { + +} + +Connection::~Connection() { + BOOST_LOG_TRIVIAL(info) << "~Connection()"; +} + +void Connection::afterConnect() { + read(); +} + +void Connection::read() { + boost::beast::http::async_read(sock_, buffer_, request_, + [self = shared_from_this()](boost::system::error_code err, size_t bytes_transferred) { + self->readHandler(err, bytes_transferred); + }); +} + +void Connection::write(std::string &command, std::function)> onWriteCb) { + // функция write может быть вызвана из разных потоков, поэтому блокируем ее до тех пор, пока другой поток не закончит запись + std::unique_lock ulock(writeMutex_); + writeCv_.wait(ulock, [this]() { return bool(canWrite_); }); + canWrite_ = false; + + response_.clear(); + + response_.keep_alive(request_.keep_alive()); + response_.version(request_.version()); + response_.result(boost::beast::http::status::ok); + response_.body() = command; + + response_.prepare_payload(); + + boost::beast::http::async_write(sock_, response_, + [self = shared_from_this(), onWriteCb](boost::system::error_code err, size_t bytes_transferred) { + self->writeHandler(err, bytes_transferred, onWriteCb); + }); +} + + +void Connection::readHandler(const boost::system::error_code &err, std::size_t bytes_transferred) { + if (err) { + BOOST_LOG_TRIVIAL(error) << "Error while read from client"; + if (onDeleteCb_) { + onDeleteCb_(shared_from_this()); + } + return; + } + + onReadCb_(shared_from_this(), request_.body().data()); + + request_.clear(); + request_.body().clear(); + + read(); +} + + +void Connection::writeHandler(const boost::system::error_code &err, std::size_t bytes_transferred, + std::function)> onWriteCb) { + if (err) { + if (onDeleteCb_) { + onDeleteCb_(shared_from_this()); + } + if (onWriteCb) { + onWriteCb(nullptr); + } + return; + } + + if (onWriteCb) { + onWriteCb(shared_from_this()); + } + + canWrite_ = true; + writeCv_.notify_one(); +} + +void Connection::stop() { + sock_.close(); +} + +boost::asio::ip::tcp::socket &Connection::getSock() { + return sock_; +} + +void Connection::setAuth(bool) { + isAuth_ = true; +} + +bool Connection::getAuth() { + return isAuth_; +} + diff --git a/project/src/MainServer.cpp b/project/src/MainServer.cpp new file mode 100644 index 0000000..b4365f1 --- /dev/null +++ b/project/src/MainServer.cpp @@ -0,0 +1,53 @@ +#include "MainServer.h" +#include "Command.h" + +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +MainServer::MainServer(int port, int countThreads) : service_(), server_(port, service_, + ServerCallbacks{ + [this](std::shared_ptr connection) { + this->handleAcceptConnection(std::move(connection)); + }, + [this](std::shared_ptr connection, + std::string &&command) { + this->onReadCb(std::move(connection), + std::move(command)); + }, {}} +), + countThreads_(countThreads) { + createNewDocumentCommand_ = new Command(CREATE_DOCUMENT); +} + +void MainServer::runServer() { + server_.runServer(); + + std::vector threads; + threads.reserve(countThreads_); + for (int i = 0; i < countThreads_; ++i) { + threads.emplace_back([this]() { + service_.run(); + }); + } + + for (auto &thread: threads) { + thread.join(); + } +} + +void MainServer::handleAcceptConnection(std::shared_ptr connection) { + BOOST_LOG_TRIVIAL(info) << "Accept new client to MainServer"; + connection->afterConnect(); +} + +void MainServer::onReadCb(std::shared_ptr connection, std::string &&command) { + auto command_parse = json::parse(command); + if (command_parse["target"] == "sharing_document") { + createNewDocumentCommand_->do_command(command_parse, std::move(connection)); + } +} diff --git a/project/src/Server.cpp b/project/src/Server.cpp new file mode 100644 index 0000000..4eb540b --- /dev/null +++ b/project/src/Server.cpp @@ -0,0 +1,49 @@ +#include +#include +#include +#include +#include + +#include "Server.h" + + +Server::Server(int port, boost::asio::io_context &service, ServerCallbacks &&callbacks) : acceptor_(service), + port_(port), + callbacks_(std::move(callbacks)) { + +} + +void Server::runServer() { + BOOST_LOG_TRIVIAL(info) << "Server run"; + boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), port_); + acceptor_.open(endpoint.protocol()); + acceptor_.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true)); + acceptor_.bind(endpoint); + acceptor_.listen(1024); + + startAcceptConnections(); +} + +int Server::getPort() { + return port_; +} + +void Server::startAcceptConnections() { + std::shared_ptr connection( + new Connection(static_cast(acceptor_.get_executor().context()), callbacks_.onReadCb_, callbacks_.onDeleteCb_)); + acceptor_.async_accept(connection->getSock(), boost::bind(&Server::handleAcceptConnection, this, + connection, boost::asio::placeholders::error)); + BOOST_LOG_TRIVIAL(info) << "Start accept connections"; +} + +void Server::handleAcceptConnection(std::shared_ptr connection, const boost::system::error_code &err) { + if (err) { + startAcceptConnections(); + } + + callbacks_.onAcceptCb_(connection); + startAcceptConnections(); +} + + + diff --git a/project/src/SharedDocumentServer.cpp b/project/src/SharedDocumentServer.cpp new file mode 100644 index 0000000..4048df4 --- /dev/null +++ b/project/src/SharedDocumentServer.cpp @@ -0,0 +1,105 @@ +#include +#include +#include + +#include "SharedDocumentServer.h" +#include "Command.h" +#include "CommandConstructor.h" +#include + +SharedDocumentServer::SharedDocumentServer(boost::asio::io_context &service) : server_(start_since_port++, service, + ServerCallbacks{ + [this](std::shared_ptr connection) { + this->handleAcceptConnection( + std::move(connection)); + }, + [this](std::shared_ptr author, + std::string &&command) { + this->onReadCb(std::move(author), + std::move(command)); + }, + [this](std::shared_ptr connection) { + this->onDeleteConnection( + std::move(connection)); + }}) { + + generateAuthToken(); + documentCommandBus_ = new DocumentCommandBus(&connections_); +} + +void SharedDocumentServer::generateAuthToken() { + static const std::vector syms = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', '_', '-', '$', '@'}; + static int token_length = 20; + for (int i = 0; i < token_length; ++i) { + authToken_ += syms[rand() % syms.size()]; + } +} + +void SharedDocumentServer::startShared() { + server_.runServer(); +} + + +bool SharedDocumentServer::checkAuthToken(std::string &&token) { + return token == authToken_; +} + + +void SharedDocumentServer::onReadCb(std::shared_ptr author, std::string &&command) { + BOOST_LOG_TRIVIAL(info) << "command from user: " << command; + //std::string command_cpy = command; + //documentCommandBus_->do_command(std::move(command_cpy), author); + auto command_parse = json::parse(command); + if (command_parse["target"] == "auth" && !author->getAuth()) { + if (checkAuthToken(command_parse["auth_token"])) { + std::string response_dump = CommandConstructor::authServer("OK", getAuthToken(), std::string("127.0.0.1"), getPort()).dump(); + author->setAuth(true); + author->write(response_dump); + BOOST_LOG_TRIVIAL(info) << "Connect new client to document"; + } else { + std::string response_dump = CommandConstructor::authServer("FAIL", "", std::string("127.0.0.1"), getPort()).dump(); + author->setAuth(false); + // если пользователь послал не верный аутентификационный токен - удаляем его + author->write(response_dump, [](std::shared_ptr connection) { connection->stop(); }); + } + } else if (author->getAuth()) { + documentCommandBus_->do_command(std::move(command), author); + } +} + + +void SharedDocumentServer::onDeleteConnection(std::shared_ptr connection) { + auto pos = connections_.begin(); + for (auto &cl: connections_) { + if (*pos == connection) { + break; + } + pos++; + } + try { + BOOST_LOG_TRIVIAL(info) << "Delete client connection"; + (*pos)->stop(); + connections_.erase(pos); + } catch (...) { + } +} + +int SharedDocumentServer::getPort() { + return server_.getPort(); +} + + +std::string SharedDocumentServer::getAuthToken() { + return authToken_; +} + +SharedDocumentServer::~SharedDocumentServer() { + delete documentCommandBus_; +} + +void SharedDocumentServer::handleAcceptConnection(std::shared_ptr connection) { + BOOST_LOG_TRIVIAL(info) << "Accept new client to document"; + connections_.push_back(connection); + connection->afterConnect(); +} diff --git a/project/src/main.cpp b/project/src/main.cpp index f330cc7..91e8e73 100644 --- a/project/src/main.cpp +++ b/project/src/main.cpp @@ -1,7 +1,12 @@ -// -// Created by tesserakt on 30.10.2021. -// +#include + +#include "SharedDocumentServer.h" +#include "MainServer.h" + +int SharedDocumentServer::start_since_port = 6070; int main() { - return 0; + srand(time(NULL)); + MainServer server(8080, 4); + server.runServer(); } \ No newline at end of file diff --git a/project/unit_tests/main.cpp b/project/unit_tests/main.cpp new file mode 100644 index 0000000..5822dd7 --- /dev/null +++ b/project/unit_tests/main.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include + + +#include "Server.h" +#include "Settings.h" +#include "SharedDocumentServer.h" + +using namespace boost::asio; + +#define SHARING_DOCUMENT_COMMAND "sharing document" +#define AUTH_COMMAND "auth_token=" + +class TestServer : public ::testing::Test { +protected: + void SetUp() { + myServer_ = new Server(5555); + myServer_->runServer(); + ip::tcp::endpoint ep(ip::address::from_string("127.0.0.1"), 5555); + sock_ = new ip::tcp::socket(service_); + sock_->connect(ep); + } + + void TearDown() { + delete myServer_; + delete sock_; + } + + Server *myServer_; + io_service service_; + ip::tcp::socket *sock_; + char buf[1024]; +}; + +class TestSharingDocument : public ::testing::Test { +protected: + void SetUp() { + SharedDocumentServer::start_since_port = 8001; + document_ = new SharedDocumentServer(); + document_->startShared(); + auth_token_ = document_->getAuthToken(); + port_ = document_->getPort(); + } + + void TearDown() { + delete document_; + } + + SharedDocumentServer *document_; + std::string auth_token_; + int port_; + io_service service_; + char buf[1024]; +}; + +size_t read_complete(char *buf, const std::error_code &err, size_t bytes_transferred) { + if (bytes_transferred > 0 && std::string(buf + bytes_transferred - 1 - std::strlen(END_STR), buf + bytes_transferred - 1) == std::string(END_STR)) { + return 0; + } + return 1; +} + +TEST_F(TestServer, test) { + std::strcpy(buf, SHARING_DOCUMENT_COMMAND); + std::strcpy(buf + std::strlen(SHARING_DOCUMENT_COMMAND), END_STR); + sock_->write_some(buffer(buf, std::strlen(SHARING_DOCUMENT_COMMAND) + std::strlen(END_STR))); + + int bytes = read(*sock_, buffer(buf), boost::bind(read_complete, buf, _1, _2)); + std::string copy(buf, bytes - 1); + int port = -1; + EXPECT_NO_THROW([&]() { port = std::stoi(copy); }); + EXPECT_GE(port, 0); + ip::tcp::socket to_shared_document_socket(service_); + ip::tcp::endpoint shared_document_ep(ip::address::from_string("127.0.0.1"), port); + EXPECT_NO_THROW(to_shared_document_socket.connect(shared_document_ep)); +} + +TEST_F(TestSharingDocument, test) { + EXPECT_EQ(port_, 8001); + + ip::tcp::endpoint shared_doc_ep(ip::address::from_string("127.0.0.1"), port_); + ip::tcp::socket client1(service_); + EXPECT_NO_THROW(client1.connect(shared_doc_ep)); // подключаемся к расшаренному документы + + // аутентификация в документе + std::strcpy(buf, AUTH_COMMAND); + std::strcpy(buf + std::strlen(AUTH_COMMAND), (document_->getAuthToken() + END_STR).c_str()); + client1.write_some(buffer(buf, std::strlen(AUTH_COMMAND) + document_->getAuthToken().length() + std::strlen(END_STR))); + + // проверка успешности аутентификации + int bytes = read(client1, buffer(buf), boost::bind(read_complete, buf, _1, _2)); + EXPECT_STREQ(buf, AUTH_SUCCESS_COMMAND); + + // подключение к документу второго клиента + ip::tcp::socket client2(service_); + EXPECT_NO_THROW(client2.connect(shared_doc_ep)); + + // аутентификация второго клиента в документе + std::strcpy(buf, AUTH_COMMAND); + std::strcpy(buf + std::strlen(AUTH_COMMAND), (document_->getAuthToken() + END_STR).c_str()); + client2.write_some(buffer(buf, std::strlen(AUTH_COMMAND) + document_->getAuthToken().length() + std::strlen(END_STR))); + + // проверка аутентификации + bytes = read(client1, buffer(buf), boost::bind(read_complete, buf, _1, _2)); + EXPECT_STREQ(buf, AUTH_SUCCESS_COMMAND); + + // отправка команды вторым клиентом + std::string command = "aboba welcomes you"; + command += END_STR; + std::strcpy(buf, command.c_str()); + client2.write_some(buffer(buf, command.length())); + + // проверка получение первым клиентом команды второго клиента + bytes = read(client1, buffer(buf), boost::bind(read_complete, buf, _1, _2)); + EXPECT_STREQ(buf, command.c_str()); +} + + +int main(int argc, char *argv[]) { + ::testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +} \ No newline at end of file