├── .github ├── dependabot.yml └── workflows │ ├── analyze.yml │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── c_cpp_properties.json └── settings.json ├── CMakeLists.txt ├── LICENSE ├── README.md ├── client ├── CMakeLists.txt ├── client.cpp ├── client.hpp └── main.cpp ├── common ├── defaults.hpp ├── message.hpp ├── utils.cpp └── utils.hpp ├── server ├── CMakeLists.txt ├── main.cpp ├── server.cpp ├── server.hpp ├── session.cpp └── session.hpp └── sonar-project.properties /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/analyze.yml: -------------------------------------------------------------------------------- 1 | name: SonarCloud 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | analyze: 8 | runs-on: ubuntu-latest 9 | 10 | env: 11 | BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install Boost 20 | run: sudo apt install libboost-all-dev 21 | 22 | - name: Install sonar-scanner and build-wrapper 23 | uses: SonarSource/sonarcloud-github-c-cpp@v3 24 | 25 | - name: Run build-wrapper 26 | run: | 27 | cmake -S . -B build 28 | build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} cmake --build build 29 | 30 | - name: Run sonar-scanner 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 34 | run: sonar-scanner --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: ['**.md'] 7 | pull_request: 8 | branches: [main] 9 | paths-ignore: ['**.md'] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | submodules: true 20 | 21 | - name: Install Dependencies 22 | run: | 23 | sudo apt install libboost-all-dev 24 | 25 | - name: Build 26 | run: | 27 | cmake -S . -B build 28 | cmake --build build 29 | ls -Gghl build/server build/client 30 | 31 | - name: Test 32 | run: | 33 | ./build/server/server & 34 | sleep 1s 35 | ./build/client/client 36 | kill -TERM $(pidof server) 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Directories 2 | build 3 | 4 | # Prerequisites 5 | *.d 6 | 7 | # Compiled Object files 8 | *.slo 9 | *.lo 10 | *.o 11 | *.obj 12 | 13 | # Precompiled Headers 14 | *.gch 15 | *.pch 16 | 17 | # Compiled Dynamic libraries 18 | *.so 19 | *.dylib 20 | *.dll 21 | 22 | # Fortran module files 23 | *.mod 24 | *.smod 25 | 26 | # Compiled Static libraries 27 | *.lai 28 | *.la 29 | *.a 30 | *.lib 31 | 32 | # Executables 33 | *.exe 34 | *.out 35 | *.app 36 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spdlog"] 2 | path = spdlog 3 | url = git@github.com:gabime/spdlog.git 4 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": ["${workspaceFolder}/**"], 6 | "defines": [], 7 | "compilerPath": "/usr/bin/clang", 8 | "cStandard": "c11", 9 | "cppStandard": "c++11", 10 | "intelliSenseMode": "linux-clang-x64", 11 | "configurationProvider": "ms-vscode.cmake-tools" 12 | } 13 | ], 14 | "version": 4 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["spdlog"], 3 | "files.associations": { 4 | "*.json": "json", 5 | "cctype": "cpp", 6 | "clocale": "cpp", 7 | "cmath": "cpp", 8 | "csignal": "cpp", 9 | "cstdarg": "cpp", 10 | "cstddef": "cpp", 11 | "cstdio": "cpp", 12 | "cstdlib": "cpp", 13 | "cstring": "cpp", 14 | "ctime": "cpp", 15 | "cwchar": "cpp", 16 | "cwctype": "cpp", 17 | "any": "cpp", 18 | "array": "cpp", 19 | "atomic": "cpp", 20 | "strstream": "cpp", 21 | "bit": "cpp", 22 | "*.tcc": "cpp", 23 | "bitset": "cpp", 24 | "chrono": "cpp", 25 | "codecvt": "cpp", 26 | "compare": "cpp", 27 | "complex": "cpp", 28 | "concepts": "cpp", 29 | "condition_variable": "cpp", 30 | "coroutine": "cpp", 31 | "cstdint": "cpp", 32 | "deque": "cpp", 33 | "list": "cpp", 34 | "map": "cpp", 35 | "set": "cpp", 36 | "unordered_map": "cpp", 37 | "vector": "cpp", 38 | "exception": "cpp", 39 | "algorithm": "cpp", 40 | "functional": "cpp", 41 | "iterator": "cpp", 42 | "memory": "cpp", 43 | "memory_resource": "cpp", 44 | "numeric": "cpp", 45 | "optional": "cpp", 46 | "random": "cpp", 47 | "ratio": "cpp", 48 | "regex": "cpp", 49 | "source_location": "cpp", 50 | "string": "cpp", 51 | "string_view": "cpp", 52 | "system_error": "cpp", 53 | "tuple": "cpp", 54 | "type_traits": "cpp", 55 | "utility": "cpp", 56 | "future": "cpp", 57 | "initializer_list": "cpp", 58 | "iomanip": "cpp", 59 | "iosfwd": "cpp", 60 | "iostream": "cpp", 61 | "istream": "cpp", 62 | "limits": "cpp", 63 | "mutex": "cpp", 64 | "new": "cpp", 65 | "ostream": "cpp", 66 | "ranges": "cpp", 67 | "sstream": "cpp", 68 | "stdexcept": "cpp", 69 | "stop_token": "cpp", 70 | "streambuf": "cpp", 71 | "thread": "cpp", 72 | "cfenv": "cpp", 73 | "cinttypes": "cpp", 74 | "typeindex": "cpp", 75 | "typeinfo": "cpp", 76 | "variant": "cpp", 77 | "*.ipp": "cpp" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | project(TcpClientServerApp VERSION 0.1.0) 4 | 5 | add_subdirectory(client) 6 | add_subdirectory(server) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Azeem Sajid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TcpClientServerApp 2 | 3 | [![ci](https://github.com/iamazeem/TcpClientServerApp/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/iamazeem/TcpClientServerApp/actions/workflows/ci.yml) 4 | [![License: MIT](https://img.shields.io/badge/license-MIT-darkgreen.svg?style=flat-square)](./LICENSE) 5 | 6 | Sample asynchronous protocol-based 7 | [TCP](https://en.wikipedia.org/wiki/Transmission_Control_Protocol) client and 8 | server apps using C++ and 9 | [Boost::Asio](https://www.boost.org/doc/libs/1_76_0/doc/html/boost_asio.html) 10 | 11 | ```mermaid 12 | sequenceDiagram 13 | autonumber 14 | participant client 15 | participant server 16 | client-->>server: connect 17 | server->>client: welcome 18 | client->>server: command 19 | activate server 20 | note right of server: execute 21 | server->>client: output 22 | deactivate server 23 | client->>server: exit 24 | server--xclient: disconnect 25 | ``` 26 | 27 | ## Dependencies 28 | 29 | - C++11 or above 30 | - [Boost::Asio](https://www.boost.org/doc/libs/1_76_0/doc/html/boost_asio.html) 31 | - [CMake](https://cmake.org/) 32 | 33 | ## Build 34 | 35 | ```shell 36 | git clone --recursive git@github.com:iamazeem/TcpClientServerApp.git 37 | cd TcpClientServerApp 38 | cmake -S . -B build 39 | cmake --build build 40 | ``` 41 | 42 | The client and server binaries will be generated under `build` directory: 43 | 44 | - `./build/server/server` 45 | - `./build/client/client` 46 | 47 | ## Run 48 | 49 | Run `server` on one terminal: 50 | 51 | ```shell 52 | ./build/server/server 53 | ``` 54 | 55 | Run `client` on another terminal: 56 | 57 | ```shell 58 | ./build/client/client 59 | ``` 60 | 61 | ## License 62 | 63 | [MIT](./LICENSE) 64 | -------------------------------------------------------------------------------- /client/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | project(client VERSION 0.1.0) 4 | 5 | set(CMAKE_CXX_STANDARD 11) 6 | set(CMAKE_CXX_STANDARD_REQUIRED True) 7 | set(CMAKE_CXX_FLAGS "-Wall -O3") 8 | 9 | include_directories( 10 | ${CMAKE_CURRENT_SOURCE_DIR} 11 | ${CMAKE_CURRENT_SOURCE_DIR}/../spdlog/include 12 | ${CMAKE_CURRENT_SOURCE_DIR}/../common 13 | ) 14 | 15 | set(SOURCES 16 | ${CMAKE_CURRENT_SOURCE_DIR}/../common/utils.cpp 17 | ${CMAKE_CURRENT_SOURCE_DIR}/client.cpp 18 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 19 | ) 20 | 21 | add_executable(${PROJECT_NAME} ${SOURCES}) 22 | 23 | find_package(Boost REQUIRED system thread) 24 | target_link_libraries(${PROJECT_NAME} ${Boost_LIBRARIES}) 25 | -------------------------------------------------------------------------------- /client/client.cpp: -------------------------------------------------------------------------------- 1 | #include "spdlog/spdlog.h" 2 | 3 | #include "client.hpp" 4 | #include "message.hpp" 5 | #include "utils.hpp" 6 | 7 | using boost::asio::ip::address; 8 | using boost::asio::placeholders::error; 9 | using boost::system::error_code; 10 | 11 | client_t::client_t(const std::string ip, const unsigned short port) noexcept 12 | : m_endpoint{address::from_string(ip), port}, 13 | m_socket{m_io_service} 14 | { 15 | } 16 | 17 | client_t::~client_t() noexcept 18 | { 19 | disconnect(); 20 | } 21 | 22 | bool client_t::start() noexcept 23 | { 24 | if (!connect()) 25 | { 26 | return false; 27 | } 28 | if (!process()) 29 | { 30 | return false; 31 | } 32 | return true; 33 | } 34 | 35 | bool client_t::connect() noexcept 36 | { 37 | spdlog::info("connecting [{}:{}]", m_endpoint.address().to_string(), m_endpoint.port()); 38 | 39 | error_code ec; 40 | m_socket.connect(m_endpoint, ec); 41 | if (ec) 42 | { 43 | spdlog::error("failed to connect, error: {}", ec.message()); 44 | return false; 45 | } 46 | 47 | spdlog::info("connected"); 48 | return true; 49 | } 50 | 51 | bool client_t::process() noexcept 52 | { 53 | // Receive welcome message 54 | spdlog::info("receiving welcome message from server"); 55 | const auto welcome_msg = recv(m_socket); 56 | if (welcome_msg.get_header().get_type() != message_t::type_t::welcome) 57 | { 58 | spdlog::info("invalid welcome message type, {}", welcome_msg.dump()); 59 | return false; 60 | } 61 | spdlog::info("welcome message received, {}", welcome_msg.dump()); 62 | 63 | // Send command request message 64 | const auto cmd = "ls test"; 65 | message_t cmd_request_msg; 66 | cmd_request_msg.set(message_t::type_t::command_request, cmd); 67 | spdlog::info("sending command request [{}]", cmd_request_msg.dump()); 68 | if (!send(m_socket, cmd_request_msg)) 69 | { 70 | spdlog::error("error while sending command request"); 71 | return false; 72 | } 73 | spdlog::info("command request sent"); 74 | 75 | // Receive command response message 76 | spdlog::info("receiving command response from server"); 77 | const auto cmd_response_msg = recv(m_socket); 78 | if (cmd_response_msg.get_header().get_type() != message_t::type_t::command_response) 79 | { 80 | spdlog::info("invalid command response type, {}", cmd_response_msg.dump()); 81 | return false; 82 | } 83 | spdlog::info("command response received: {}", cmd_response_msg.dump()); 84 | 85 | // Send EXIT message 86 | message_t exit_msg; 87 | exit_msg.set(message_t::type_t::exit, "EXIT"); 88 | spdlog::info("sending exit message to server [{}]", exit_msg.dump()); 89 | if (!send(m_socket, exit_msg)) 90 | { 91 | spdlog::error("error while sending command request"); 92 | return false; 93 | } 94 | spdlog::info("exit message sent"); 95 | 96 | spdlog::info("session completed"); 97 | return true; 98 | } 99 | 100 | void client_t::disconnect() noexcept 101 | { 102 | if (m_socket.is_open()) 103 | { 104 | m_socket.close(); 105 | spdlog::info("disconnected"); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /client/client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | using boost::asio::io_service; 6 | using boost::asio::ip::tcp; 7 | using boost::system::error_code; 8 | 9 | class client_t final 10 | { 11 | public: 12 | client_t(const std::string ip, const unsigned short port) noexcept; 13 | ~client_t() noexcept; 14 | 15 | bool start() noexcept; 16 | 17 | private: 18 | bool connect() noexcept; 19 | bool process() noexcept; 20 | void disconnect() noexcept; 21 | 22 | io_service m_io_service; 23 | tcp::endpoint m_endpoint; 24 | tcp::socket m_socket; 25 | }; 26 | -------------------------------------------------------------------------------- /client/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "spdlog/spdlog.h" 3 | #include "defaults.hpp" 4 | #include "client.hpp" 5 | 6 | int main() 7 | { 8 | spdlog::default_logger()->set_pattern("[CLIENT] %+"); 9 | spdlog::default_logger()->set_level(spdlog::level::debug); 10 | 11 | client_t client{defaults::server::ip, defaults::server::port}; 12 | if (!client.start()) 13 | { 14 | return EXIT_FAILURE; 15 | } 16 | return EXIT_SUCCESS; 17 | } 18 | -------------------------------------------------------------------------------- /common/defaults.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace defaults 4 | { 5 | namespace server 6 | { 7 | static constexpr auto ip = "127.0.0.1"; 8 | static constexpr unsigned short port = 9900; 9 | static constexpr unsigned short threads = 10; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /common/message.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class message_t final 8 | { 9 | public: 10 | static constexpr uint16_t version = 123; 11 | 12 | enum class type_t : uint16_t 13 | { 14 | invalid, 15 | welcome, 16 | command_request, 17 | command_response, 18 | exit 19 | }; 20 | 21 | class header_t final 22 | { 23 | public: 24 | void set(const uint16_t version, 25 | const type_t type, 26 | const uint32_t payload_size) noexcept 27 | { 28 | m_version = version; 29 | m_type = type; 30 | m_payload_size = payload_size; 31 | } 32 | 33 | uint16_t get_version() const noexcept { return m_version; } 34 | type_t get_type() const noexcept { return m_type; } 35 | uint32_t get_payload_size() const noexcept { return m_payload_size; } 36 | 37 | std::string get_type_as_string() const noexcept 38 | { 39 | switch (get_type()) 40 | { 41 | case type_t::invalid: 42 | return "invalid"; 43 | case type_t::welcome: 44 | return "welcome"; 45 | case type_t::command_request: 46 | return "command_request"; 47 | case type_t::command_response: 48 | return "command_response"; 49 | case type_t::exit: 50 | return "exit"; 51 | default: 52 | return "unknown [" + std::to_string(static_cast(get_type())) + "]"; 53 | } 54 | } 55 | 56 | bool is_valid_version() const noexcept { return (get_version() == version); } 57 | bool is_valid_type() const noexcept 58 | { 59 | switch (get_type()) 60 | { 61 | case type_t::welcome: 62 | case type_t::command_request: 63 | case type_t::command_response: 64 | case type_t::exit: 65 | return true; 66 | default: 67 | return false; 68 | } 69 | } 70 | 71 | bool is_valid() const noexcept { return is_valid_version() && is_valid_type(); } 72 | 73 | std::string dump() const noexcept 74 | { 75 | std::ostringstream oss; 76 | oss << "{version: " 77 | << get_version() << ", type: " 78 | << get_type_as_string() << ", payload size: " 79 | << get_payload_size() << "}"; 80 | return oss.str(); 81 | } 82 | 83 | private: 84 | uint16_t m_version{version}; 85 | type_t m_type{type_t::invalid}; 86 | uint32_t m_payload_size{0}; 87 | }; 88 | 89 | message_t() = default; 90 | 91 | message_t(const header_t &header, const std::string &payload) noexcept 92 | : m_header{header}, 93 | m_payload{payload} 94 | { 95 | } 96 | 97 | void set(const type_t type, const std::string &payload) noexcept 98 | { 99 | m_header.set(version, type, payload.size()); 100 | m_payload = payload; 101 | } 102 | 103 | const header_t &get_header() const noexcept { return m_header; } 104 | std::string get_payload() const noexcept { return m_payload; } 105 | 106 | std::string dump() const noexcept 107 | { 108 | if (m_payload.empty()) 109 | { 110 | return m_header.dump(); 111 | } 112 | return m_header.dump() + " | payload: [" + m_payload + "]"; 113 | } 114 | 115 | private: 116 | header_t m_header{}; 117 | std::string m_payload{}; 118 | }; 119 | -------------------------------------------------------------------------------- /common/utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "spdlog/spdlog.h" 4 | #include "utils.hpp" 5 | 6 | using boost::asio::buffer; 7 | using boost::asio::read; 8 | using boost::asio::streambuf; 9 | using boost::asio::transfer_exactly; 10 | using boost::asio::write; 11 | using boost::system::error_code; 12 | 13 | std::string get_peer_ip(const tcp::socket &socket) noexcept 14 | { 15 | return socket.remote_endpoint().address().to_string(); 16 | } 17 | 18 | unsigned short get_peer_port(const tcp::socket &socket) noexcept 19 | { 20 | return socket.remote_endpoint().port(); 21 | } 22 | 23 | message_t recv(tcp::socket &socket) noexcept 24 | { 25 | spdlog::debug("receiving message"); 26 | 27 | message_t::header_t header; 28 | 29 | error_code ec; 30 | read(socket, buffer(&header, sizeof(header)), transfer_exactly(sizeof(header)), ec); 31 | if (ec) 32 | { 33 | spdlog::error("header read error, {}", ec.message()); 34 | return {}; 35 | } 36 | 37 | spdlog::debug("header: {}", header.dump()); 38 | 39 | if (!header.is_valid()) 40 | { 41 | spdlog::error("invalid header"); 42 | return {}; 43 | } 44 | 45 | if (header.get_payload_size() == 0) 46 | { 47 | return {header, ""}; 48 | } 49 | 50 | streambuf payload_buffer; 51 | read(socket, payload_buffer, transfer_exactly(header.get_payload_size()), ec); 52 | if (ec) 53 | { 54 | spdlog::error("payload read error, {}", ec.message()); 55 | return {}; 56 | } 57 | 58 | const std::string payload((std::istreambuf_iterator(&payload_buffer)), 59 | std::istreambuf_iterator()); 60 | 61 | const auto message = message_t{header, payload}; 62 | spdlog::debug("message received: {}", message.dump()); 63 | return message; 64 | } 65 | 66 | bool send(tcp::socket &socket, const message_t &message) noexcept 67 | { 68 | spdlog::debug("sending message: {}", message.dump()); 69 | 70 | const auto header = message.get_header(); 71 | error_code ec; 72 | write(socket, buffer(&header, sizeof(header)), transfer_exactly(sizeof(header)), ec); 73 | if (ec) 74 | { 75 | spdlog::error("header send error, {}", ec.message()); 76 | return false; 77 | } 78 | 79 | if (header.get_payload_size() > 0) 80 | { 81 | const auto payload = message.get_payload(); 82 | write(socket, buffer(payload, payload.size()), transfer_exactly(payload.size()), ec); 83 | if (ec) 84 | { 85 | spdlog::error("payload send error, {}", ec.message()); 86 | return false; 87 | } 88 | } 89 | 90 | spdlog::debug("message sent [{}:{}]", get_peer_ip(socket), get_peer_port(socket)); 91 | return true; 92 | } 93 | -------------------------------------------------------------------------------- /common/utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "message.hpp" 6 | 7 | using boost::asio::ip::tcp; 8 | 9 | std::string get_peer_ip(const tcp::socket &socket) noexcept; 10 | unsigned short get_peer_port(const tcp::socket &socket) noexcept; 11 | 12 | message_t recv(tcp::socket &socket) noexcept; 13 | bool send(tcp::socket &socket, const message_t &message) noexcept; 14 | -------------------------------------------------------------------------------- /server/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | project(server VERSION 0.1.0) 4 | 5 | set(CMAKE_CXX_STANDARD 11) 6 | set(CMAKE_CXX_STANDARD_REQUIRED True) 7 | set(CMAKE_CXX_FLAGS "-Wall -O3") 8 | 9 | include_directories( 10 | ${CMAKE_CURRENT_SOURCE_DIR} 11 | ${CMAKE_CURRENT_SOURCE_DIR}/../spdlog/include 12 | ${CMAKE_CURRENT_SOURCE_DIR}/../common 13 | ) 14 | 15 | set(SOURCES 16 | ${CMAKE_CURRENT_SOURCE_DIR}/../common/utils.cpp 17 | ${CMAKE_CURRENT_SOURCE_DIR}/session.cpp 18 | ${CMAKE_CURRENT_SOURCE_DIR}/server.cpp 19 | ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp 20 | ) 21 | 22 | add_executable(${PROJECT_NAME} ${SOURCES}) 23 | 24 | find_package(Boost REQUIRED system thread) 25 | target_link_libraries(${PROJECT_NAME} ${Boost_LIBRARIES}) 26 | -------------------------------------------------------------------------------- /server/main.cpp: -------------------------------------------------------------------------------- 1 | #include "spdlog/spdlog.h" 2 | 3 | #include "defaults.hpp" 4 | #include "server.hpp" 5 | 6 | int main() 7 | { 8 | spdlog::default_logger()->set_pattern("[SERVER] %+"); 9 | spdlog::default_logger()->set_level(spdlog::level::debug); 10 | 11 | server_t server{defaults::server::ip, defaults::server::port, defaults::server::threads}; 12 | if (!server.start()) 13 | { 14 | return EXIT_FAILURE; 15 | } 16 | return EXIT_SUCCESS; 17 | } 18 | -------------------------------------------------------------------------------- /server/server.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define BOOST_BIND_GLOBAL_PLACEHOLDERS 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "spdlog/spdlog.h" 10 | #include "server.hpp" 11 | #include "utils.hpp" 12 | 13 | using boost::shared_ptr; 14 | using boost::asio::ip::address; 15 | using boost::asio::placeholders::error; 16 | using boost::system::error_code; 17 | 18 | server_t::server_t( 19 | const std::string ip, const unsigned short port, 20 | const unsigned short num_threads) noexcept 21 | : m_ios_acceptors{boost::make_shared()}, 22 | m_ios_work_acceptors{boost::make_shared(*m_ios_acceptors)}, 23 | m_ios_executors{boost::make_shared()}, 24 | m_ios_work_executors{boost::make_shared(*m_ios_executors)}, 25 | m_endpoint{address::from_string(ip), port}, 26 | m_acceptor{*m_ios_acceptors, m_endpoint}, 27 | m_session{boost::make_shared(m_ios_executors)}, 28 | m_signals{*m_ios_acceptors, SIGINT, SIGTERM} 29 | { 30 | // Add signal handling for graceful termination (CTRL + C) 31 | m_signals.async_wait(boost::bind(&server_t::stop, this)); 32 | 33 | spdlog::info("starting [{}:{}]", ip, port); 34 | 35 | for (unsigned int i = 0; i < num_threads; ++i) 36 | { 37 | m_executors_thread_group.create_thread(boost::bind(&server_t::worker_thread_callback, 38 | this, 39 | m_ios_executors)); 40 | } 41 | 42 | m_acceptor.async_accept(m_session->get_socket(), 43 | boost::bind(&server_t::accept_handler, this, m_session, error)); 44 | 45 | spdlog::info("started, press CTRL+C to quit"); 46 | } 47 | 48 | server_t::~server_t() noexcept 49 | { 50 | stop(); 51 | spdlog::info("server stopped successfully"); 52 | } 53 | 54 | bool server_t::start() noexcept 55 | { 56 | error_code ec; 57 | m_ios_acceptors->run(ec); 58 | if (ec) 59 | { 60 | spdlog::error("error: {}", ec.message()); 61 | return false; 62 | } 63 | return true; 64 | } 65 | 66 | void server_t::stop() noexcept 67 | { 68 | if (!m_ios_acceptors->stopped()) 69 | { 70 | m_ios_acceptors->stop(); 71 | } 72 | 73 | if (!m_ios_executors->stopped()) 74 | { 75 | m_ios_executors->stop(); 76 | m_executors_thread_group.interrupt_all(); 77 | m_executors_thread_group.join_all(); 78 | } 79 | } 80 | 81 | // Utility methods 82 | 83 | void server_t::worker_thread_callback(boost::shared_ptr ios) noexcept 84 | { 85 | error_code ec; 86 | ios->run(ec); 87 | if (ec) 88 | { 89 | spdlog::error("callback error: {}", ec.message()); 90 | } 91 | } 92 | 93 | void server_t::accept_handler(boost::shared_ptr this_session, const error_code &ec) noexcept 94 | { 95 | if (!ec) 96 | { 97 | const auto client_ip = get_peer_ip(this_session->get_socket()); 98 | const auto client_port = get_peer_port(this_session->get_socket()); 99 | spdlog::info("new client connected [{}:{}]", client_ip, client_port); 100 | 101 | m_ios_executors->post(boost::bind(&session_t::start, this_session)); 102 | 103 | m_session = boost::make_shared(m_ios_executors); 104 | m_acceptor.async_accept(m_session->get_socket(), 105 | boost::bind(&server_t::accept_handler, this, m_session, error)); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /server/server.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "session.hpp" 11 | 12 | using boost::enable_shared_from_this; 13 | using boost::shared_ptr; 14 | using boost::asio::io_service; 15 | using boost::asio::ip::tcp; 16 | using boost::system::error_code; 17 | 18 | class server_t final 19 | { 20 | public: 21 | server_t( 22 | const std::string ip, const unsigned short port, 23 | const unsigned short num_threads) noexcept; 24 | ~server_t() noexcept; 25 | 26 | bool start() noexcept; 27 | void stop() noexcept; 28 | 29 | private: 30 | void worker_thread_callback(shared_ptr ios) noexcept; 31 | void accept_handler(shared_ptr this_session, const error_code &ec) noexcept; 32 | void accept_new_connection() noexcept; 33 | 34 | shared_ptr m_ios_acceptors; 35 | shared_ptr m_ios_work_acceptors; 36 | shared_ptr m_ios_executors; 37 | shared_ptr m_ios_work_executors; 38 | boost::thread_group m_executors_thread_group; 39 | 40 | tcp::endpoint m_endpoint; 41 | tcp::acceptor m_acceptor; 42 | 43 | shared_ptr m_session; 44 | 45 | boost::asio::signal_set m_signals; 46 | }; 47 | -------------------------------------------------------------------------------- /server/session.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "spdlog/spdlog.h" 3 | #include "session.hpp" 4 | #include "message.hpp" 5 | #include "utils.hpp" 6 | 7 | session_t::session_t(shared_ptr ios) noexcept 8 | : m_io_service{ios}, 9 | m_socket{*m_io_service} 10 | { 11 | } 12 | 13 | session_t::~session_t() noexcept 14 | { 15 | if (m_socket.is_open()) 16 | { 17 | m_socket.shutdown(boost::asio::socket_base::shutdown_both); 18 | m_socket.close(); 19 | } 20 | } 21 | 22 | bool session_t::start() noexcept 23 | { 24 | spdlog::info("session started"); 25 | if (!process()) 26 | { 27 | return false; 28 | } 29 | spdlog::info("session completed"); 30 | return true; 31 | } 32 | 33 | bool session_t::process() noexcept 34 | { 35 | if (!welcome_client()) 36 | { 37 | return false; 38 | } 39 | 40 | do 41 | { 42 | spdlog::info("receiving command message from client"); 43 | const auto msg = recv(m_socket); 44 | if ((msg.get_header().get_type() == message_t::type_t::exit) && 45 | (msg.get_payload() == "EXIT")) 46 | { 47 | spdlog::info("exit message received"); 48 | break; 49 | } 50 | else if (msg.get_header().get_type() == message_t::type_t::command_request) 51 | { 52 | if (!process_command(msg.get_payload())) 53 | { 54 | spdlog::info("error while process command request message"); 55 | return false; 56 | } 57 | spdlog::info("command processing completed"); 58 | } 59 | else 60 | { 61 | spdlog::error("invalid message type received"); 62 | return false; 63 | } 64 | } while (m_socket.is_open()); 65 | return true; 66 | } 67 | 68 | bool session_t::welcome_client() noexcept 69 | { 70 | std::ostringstream oss; 71 | oss << "Welcome! [" << get_peer_ip(m_socket) << ":" << get_peer_port(m_socket) << "]"; 72 | 73 | message_t welcome_msg; 74 | welcome_msg.set(message_t::type_t::welcome, oss.str()); 75 | spdlog::info("sending welcome message to client"); 76 | if (!send(m_socket, welcome_msg)) 77 | { 78 | spdlog::error("error while sending welcome message"); 79 | return false; 80 | } 81 | spdlog::info("welcome message sent"); 82 | return true; 83 | } 84 | 85 | bool session_t::process_command(const std::string &cmd) noexcept 86 | { 87 | spdlog::info("processing command [{}]", cmd); 88 | 89 | const auto exit_status = std::system(cmd.data()); 90 | const auto cmd_response_payload = (exit_status == 0 ? "SUCCESS" : "FAILURE"); 91 | 92 | spdlog::info("sending command response message"); 93 | message_t cmd_response_msg; 94 | cmd_response_msg.set(message_t::type_t::command_response, cmd_response_payload); 95 | if (!send(m_socket, cmd_response_msg)) 96 | { 97 | spdlog::error("error while sending command response message"); 98 | return false; 99 | } 100 | spdlog::info("command response message sent"); 101 | return true; 102 | } 103 | -------------------------------------------------------------------------------- /server/session.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | using boost::shared_ptr; 8 | using boost::asio::io_service; 9 | using boost::asio::ip::tcp; 10 | 11 | class session_t final 12 | { 13 | public: 14 | session_t(shared_ptr ios) noexcept; 15 | ~session_t() noexcept; 16 | 17 | bool start() noexcept; 18 | 19 | tcp::socket &get_socket() noexcept { return m_socket; } 20 | 21 | private: 22 | bool process() noexcept; 23 | bool welcome_client() noexcept; 24 | bool process_command(const std::string &cmd) noexcept; 25 | 26 | shared_ptr m_io_service; 27 | tcp::socket m_socket; 28 | }; 29 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=iamazeem_TcpClientServerApp 2 | sonar.organization=iamazeem 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=TcpClientServerApp 6 | #sonar.projectVersion=1.0 7 | 8 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 9 | #sonar.sources=. 10 | 11 | # Encoding of the source code. Default is default system encoding 12 | #sonar.sourceEncoding=UTF-8 13 | --------------------------------------------------------------------------------