Repository: elhayra/tcp_server_client Branch: master Commit: 34c8c5e1e391 Files: 26 Total size: 41.3 KB Directory structure: gitextract_ndbhzy7z/ ├── .dockerignore ├── .github/ │ └── workflows/ │ └── cmake.yml ├── .gitignore ├── .ignore ├── CMakeLists.txt ├── Dockerfile ├── LICENSE ├── README.md ├── build.sh ├── docker-compose.yml ├── examples/ │ ├── client_example.cpp │ └── server_example.cpp ├── include/ │ ├── client.h │ ├── client_event.h │ ├── client_observer.h │ ├── common.h │ ├── file_descriptor.h │ ├── pipe_ret_t.h │ ├── server_observer.h │ ├── tcp_client.h │ └── tcp_server.h └── src/ ├── client.cpp ├── common.cpp ├── pipe_ret_t.cpp ├── tcp_client.cpp └── tcp_server.cpp ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ cmake-build cmake-build-debug build ================================================ FILE: .github/workflows/cmake.yml ================================================ name: CMake on: push: branches: [ master ] pull_request: branches: [ master ] env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Release jobs: build: # The CMake configure and build commands are platform agnostic and should work equally # well on Windows or Mac. You can convert this to a matrix build if you need # cross-platform coverage. # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} - name: Build # Build your program with the given configuration run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - name: Test working-directory: ${{github.workspace}}/build # Execute tests defined by the CMake configuration. # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail run: ctest -C ${{env.BUILD_TYPE}} ================================================ FILE: .gitignore ================================================ /build/ .idea/ cmake-build-debug/ cmake-build/ ================================================ FILE: .ignore ================================================ cmake-build-debug/ .idea/ ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.8.1) project(tcp_client_server) find_package (Threads) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} "-std=c++11") add_library(${PROJECT_NAME} src/tcp_client.cpp src/tcp_server.cpp src/client.cpp src/pipe_ret_t.cpp src/common.cpp) option(SERVER_EXAMPLE "Build SERVER" ON) if(SERVER_EXAMPLE) add_definitions( -DSERVER_EXAMPLE ) add_executable(tcp_server examples/server_example.cpp) target_link_libraries (tcp_server ${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) endif() option(CLIENT_EXAMPLE "Build CLIENT" ON) if(CLIENT_EXAMPLE) add_definitions( -DCLIENT_EXAMPLE ) add_executable(tcp_client examples/client_example.cpp) target_link_libraries (tcp_client ${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) endif() ================================================ FILE: Dockerfile ================================================ FROM alpine:3.8 RUN set -ex && \ apk add --no-cache gcc musl-dev cmake cmake clang clang-dev make g++ libc-dev linux-headers WORKDIR /usr/src/tcp_server_client COPY . . RUN chmod +x build.sh RUN ./build.sh EXPOSE 65123 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Elhay Rauper Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![cmake workflow](https://github.com/elhayra/tcp_server_client/actions/workflows/cmake.yml/badge.svg) # TCP server client A thin and simple C++ TCP client and server library with examples. ### Platforms Support Currently, both linux and mac are supported ### Examples Simple tcp server-client examples. They are optimized for simplicity and ease of use/read but not for performance. However, I believe tuning this code to suite your needs should be easy in most cases. You can find code examples of both server and client under the 'examples' directory. Both the library and the examples are well documented. ### Server The server is thread-safe, and can handle multiple clients at the same time, and remove dead clients resources automatically. ## Quick start build the examples and static library file: ```bash $ cd tcp_server_client $ ./build ``` run the server and client examples: Navigate into the `build` folder and run in the terminal: ```bash $ cd build ``` In terminal #1: ```bash $ ./tcp_server ``` In terminal #2: ```bash $ ./tcp_client ``` ## Building This project is set to use CMake to build both the *client example* and the *server example*. In addition, CMake builds a static *library file* to hold the common code to both server and client. In order to build the project you can either use the `build.sh` script: ```bash $ cd tcp_server_client $ ./build ``` or build it manually: ```bash $ cd tcp_server_client $ mkdir build $ cmake .. $ make ``` The build process generate three files: `libtcp_client_server.a`, `tcp_client` and `tcp_server`. The last two are the executables of the examples which can be executed in the terminal. #### Building Only Server or Client By default, the CMake configuration builds both server and client. However, you can use flags to build only one of the apps as follows: ###### Disabling the server build To build only the client, disable the server build by replace the `cmake ..` call by: ```bash cmake -DSERVER_EXAMPLE=OFF .. ``` And then run the make command as usual. ###### Disabling the client build To build only the server, disable the client build by replace the `cmake ..` call by: ```bash cmake -DCLIENT_EXAMPLE=OFF .. ``` And then run the make command as usual. ## Run the Examples Navigate into the `build` folder and run in the terminal: ```bash $ ./tcp_server ``` You should see a menu output on the screen, we'll get back to that. In a different terminal, run the client: ````bash $ ./tcp_client ```` You should see a similar menu for the client too. In addition, as mentioned, the client will try to connect to the server right away, so you should also see an output messages on both client and server terminals indicating that the connection succeeded. Now, feel free to play with each of the client/server menus. You can exchange messages b/w client and server, print active clients etc. You can also spawn more clients in other terminals to see how the server handles multiple clients. ## Server-Client API After playing with the examples, go into the examples source code and have a look at the `main()` function to learn how the server and client interacts. The examples are heavily documented. In addition, you can also look at the public functions in `tcp_client.h` and `tcp_server.h` to learn what APIs are available. ### Server and Client Events Both server and client are using the observer design pattern to register and handle events. When registering to an event with a callback, you should make sure that: - The callback is fast (not doing any heavy lifting tasks) because those callbacks are called from the context of the server or client. - No server / client function calls are made in those callbacks to avoid possible deadlock. ================================================ FILE: build.sh ================================================ #!/bin/sh mkdir -p build && \ cd build && \ cmake .. && make ================================================ FILE: docker-compose.yml ================================================ services: tcp-server: build: context: . dockerfile: Dockerfile command: sh -c "cd build && ./tcp_server" tcp-client: build: context: . dockerfile: Dockerfile command: sh -c "cd build && ./tcp_client" ================================================ FILE: examples/client_example.cpp ================================================ /////////////////////////////////////////////////////////// /////////////////////CLIENT EXAMPLE//////////////////////// /////////////////////////////////////////////////////////// #ifdef CLIENT_EXAMPLE #include #include #include "../include/tcp_client.h" TcpClient client; // on sig_exit, close client void sig_exit(int s) { std::cout << "Closing client...\n"; pipe_ret_t finishRet = client.close(); if (finishRet.isSuccessful()) { std::cout << "Client closed.\n"; } else { std::cout << "Failed to close client.\n"; } exit(0); } // observer callback. will be called for every new message received by the server void onIncomingMsg(const char * msg, size_t size) { std::cout << "Got msg from server: " << msg << "\n"; } // observer callback. will be called when server disconnects void onDisconnection(const pipe_ret_t & ret) { std::cout << "Server disconnected: " << ret.message() << "\n"; } void printMenu() { std::cout << "select one of the following options: \n" << "1. send message to server\n" << "2. close client and exit\n"; } int getMenuSelection() { int selection = 0; std::cin >> selection; if (!std::cin) { throw std::runtime_error("invalid menu input. expected a number, but got something else"); } std::cin.ignore (std::numeric_limits::max(), '\n'); return selection; } bool handleMenuSelection(int selection) { static const int minSelection = 1; static const int maxSelection = 2; if (selection < minSelection || selection > maxSelection) { std::cout << "invalid selection: " << selection << ". selection must be b/w " << minSelection << " and " << maxSelection << "\n"; return false; } switch (selection) { case 1: { // send message to server std::cout << "enter message to send:\n"; std::string message; std::cin >> message; pipe_ret_t sendRet = client.sendMsg(message.c_str(), message.size()); if (!sendRet.isSuccessful()) { std::cout << "Failed to send message: " << sendRet.message() << "\n"; } else { std::cout << "message was sent successfuly\n"; } break; } case 2: { // close client const pipe_ret_t closeResult = client.close(); if (!closeResult.isSuccessful()) { std::cout << "closing client failed: " << closeResult.message() << "\n"; } else { std::cout << "closed client successfully\n"; } return true; } default: { std::cout << "invalid selection: " << selection << ". selection must be b/w " << minSelection << " and " << maxSelection << "\n"; } } return false; } int main() { //register to SIGINT to close client when user press ctrl+c signal(SIGINT, sig_exit); // configure and register observer client_observer_t observer; observer.wantedIP = "127.0.0.1"; observer.incomingPacketHandler = onIncomingMsg; observer.disconnectionHandler = onDisconnection; client.subscribe(observer); // connect client to an open server bool connected = false; while (!connected) { pipe_ret_t connectRet = client.connectTo("127.0.0.1", 65123); connected = connectRet.isSuccessful(); if (connected) { std::cout << "Client connected successfully\n"; } else { std::cout << "Client failed to connect: " << connectRet.message() << "\n" << "Make sure the server is open and listening\n\n"; sleep(2); std::cout << "Retrying to connect...\n"; } }; // send messages to server bool shouldTerminate = false; while(!shouldTerminate) { printMenu(); int selection = getMenuSelection(); shouldTerminate = handleMenuSelection(selection); } return 0; } #endif ================================================ FILE: examples/server_example.cpp ================================================ /////////////////////////////////////////////////////////// /////////////////////SERVER EXAMPLE//////////////////////// /////////////////////////////////////////////////////////// #ifdef SERVER_EXAMPLE #include #include #include "../include/tcp_server.h" // declare the server TcpServer server; // declare a server observer which will receive incomingPacketHandler messages. // the server supports multiple observers server_observer_t observer1, observer2; // observer callback. will be called for every new message received by clients // with the requested IP address void onIncomingMsg1(const std::string &clientIP, const char * msg, size_t size) { std::string msgStr = msg; // print client message std::cout << "Observer1 got client msg: " << msgStr << "\n"; } // observer callback. will be called for every new message received by clients // with the requested IP address void onIncomingMsg2(const std::string &clientIP, const char * msg, size_t size) { std::string msgStr = msg; // print client message std::cout << "Observer2 got client msg: " << msgStr << "\n"; } // observer callback. will be called when client disconnects void onClientDisconnected(const std::string &ip, const std::string &msg) { std::cout << "Client: " << ip << " disconnected. Reason: " << msg << "\n"; } // accept a single client. // if you wish to accept multiple clients, call this function in a loop // (you might want to use a thread to accept clients without blocking) void acceptClient() { try { std::cout << "waiting for incoming client...\n"; std::string clientIP = server.acceptClient(0); std::cout << "accepted new client with IP: " << clientIP << "\n" << "== updated list of accepted clients ==" << "\n"; server.printClients(); } catch (const std::runtime_error &error) { std::cout << "Accepting client failed: " << error.what() << "\n"; } } void printMenu() { std::cout << "\n\nselect one of the following options: \n" << "1. send all clients a message\n" << "2. print list of accepted clients\n" << "3. send message to a specific client\n" << "4. close server and exit\n"; } int getMenuSelection() { int selection = 0; std::cin >> selection; if (!std::cin) { throw std::runtime_error("invalid menu input. expected a number, but got something else"); } std::cin.ignore (std::numeric_limits::max(), '\n'); return selection; } /** * handle menu selection and return true in case program should terminate * after handling selection */ bool handleMenuSelection(int selection) { static const int minSelection = 1; static const int maxSelection = 4; if (selection < minSelection || selection > maxSelection) { std::cout << "invalid selection: " << selection << ". selection must be b/w " << minSelection << " and " << maxSelection << "\n"; return false; } switch (selection) { case 1: { // send all clients a message std::string msg; std::cout << "type message to send to all connected clients:\n"; getline(std::cin, msg); pipe_ret_t sendingResult = server.sendToAllClients(msg.c_str(), msg.size()); if (sendingResult.isSuccessful()) { std::cout << "sent message to all clients successfully\n"; } else { std::cout << "failed to sent message: " << sendingResult.message() << "\n"; } break; } case 2: { // print list of accepted clients server.printClients(); break; } case 3: { // send message to a specific client std::cout << "enter client IP:\n"; std::string clientIP; std::cin >> clientIP; std::cout << "enter message to send:\n"; std::string message; std::cin >> message; pipe_ret_t result = server.sendToClient(clientIP, message.c_str(), message.size()); if (!result.isSuccessful()) { std::cout << "sending failed: " << result.message() << "\n"; } else { std::cout << "sending succeeded\n"; } break; }; case 4: { // close server pipe_ret_t sendingResult = server.close(); if (sendingResult.isSuccessful()) { std::cout << "closed server successfully\n"; } else { std::cout << "failed to close server: " << sendingResult.message() << "\n"; } return true; } default: { std::cout << "invalid selection: " << selection << ". selection must be b/w " << minSelection << " and " << maxSelection << "\n"; } } return false; } int main() { // start server on port 65123 pipe_ret_t startRet = server.start(65123); if (startRet.isSuccessful()) { std::cout << "Server setup succeeded\n"; } else { std::cout << "Server setup failed: " << startRet.message() << "\n"; return EXIT_FAILURE; } // configure and register observer1 observer1.incomingPacketHandler = onIncomingMsg1; observer1.disconnectionHandler = onClientDisconnected; observer1.wantedIP = "127.0.0.1"; server.subscribe(observer1); // configure and register observer2 observer2.incomingPacketHandler = onIncomingMsg2; observer2.disconnectionHandler = nullptr; // nullptr or not setting this means we don't care about disconnection event observer2.wantedIP = "10.88.0.11"; // use empty string instead to receive messages from any IP address server.subscribe(observer2); acceptClient(); bool shouldTerminate = false; while(!shouldTerminate) { printMenu(); int selection = getMenuSelection(); shouldTerminate = handleMenuSelection(selection); } return 0; } #endif ================================================ FILE: include/client.h ================================================ #pragma once #include #include #include #include #include #include "pipe_ret_t.h" #include "client_event.h" #include "file_descriptor.h" class Client { using client_event_handler_t = std::function; private: FileDescriptor _sockfd; std::string _ip = ""; std::atomic _isConnected; std::thread * _receiveThread = nullptr; client_event_handler_t _eventHandlerCallback; void setConnected(bool flag) { _isConnected = flag; } void receiveTask(); void terminateReceiveThread(); public: Client(int); bool operator ==(const Client & other) const ; void setIp(const std::string & ip) { _ip = ip; } std::string getIp() const { return _ip; } void setEventsHandler(const client_event_handler_t & eventHandler) { _eventHandlerCallback = eventHandler; } void publishEvent(ClientEvent clientEvent, const std::string &msg = ""); bool isConnected() const { return _isConnected; } void startListen(); void send(const char * msg, size_t msgSize) const; void close(); void print() const; }; ================================================ FILE: include/client_event.h ================================================ #pragma once enum ClientEvent { DISCONNECTED, INCOMING_MSG }; ================================================ FILE: include/client_observer.h ================================================ #pragma once #include #include #include "pipe_ret_t.h" struct client_observer_t { std::string wantedIP = ""; std::function incomingPacketHandler = nullptr; std::function disconnectionHandler = nullptr; }; ================================================ FILE: include/common.h ================================================ #pragma once #include #define MAX_PACKET_SIZE 4096 namespace fd_wait { enum Result { FAILURE, TIMEOUT, SUCCESS }; Result waitFor(const FileDescriptor &fileDescriptor, uint32_t timeoutSeconds = 1); }; ================================================ FILE: include/file_descriptor.h ================================================ #pragma once class FileDescriptor { private: int _sockfd = 0; public: void set(int fd) { _sockfd = fd; } int get() const { return _sockfd; } }; ================================================ FILE: include/pipe_ret_t.h ================================================ #pragma once class pipe_ret_t { private: bool _successFlag = false; std::string _msg = ""; public: pipe_ret_t() = default; pipe_ret_t(bool successFlag, const std::string &msg) : _successFlag{successFlag}, _msg{msg} {} std::string message() const { return _msg; } bool isSuccessful() const { return _successFlag; } static pipe_ret_t failure(const std::string & msg); static pipe_ret_t success(const std::string &msg = ""); }; ================================================ FILE: include/server_observer.h ================================================ #pragma once #include #include #include "client.h" struct server_observer_t { std::string wantedIP = ""; std::function incomingPacketHandler; std::function disconnectionHandler; }; ================================================ FILE: include/tcp_client.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "client_observer.h" #include "pipe_ret_t.h" #include "file_descriptor.h" class TcpClient { private: FileDescriptor _sockfd; std::atomic _isConnected; std::atomic _isClosed; struct sockaddr_in _server; std::vector _subscibers; std::thread * _receiveTask = nullptr; std::mutex _subscribersMtx; void initializeSocket(); void startReceivingMessages(); void setAddress(const std::string& address, int port); void publishServerMsg(const char * msg, size_t msgSize); void publishServerDisconnected(const pipe_ret_t & ret); void receiveTask(); void terminateReceiveThread(); public: TcpClient(); ~TcpClient(); pipe_ret_t connectTo(const std::string & address, int port); pipe_ret_t sendMsg(const char * msg, size_t size); void subscribe(const client_observer_t & observer); bool isConnected() const { return _isConnected; } pipe_ret_t close(); }; ================================================ FILE: include/tcp_server.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "client.h" #include "server_observer.h" #include "pipe_ret_t.h" #include "file_descriptor.h" class TcpServer { private: FileDescriptor _sockfd; struct sockaddr_in _serverAddress; struct sockaddr_in _clientAddress; fd_set _fds; std::vector _clients; std::vector _subscribers; std::mutex _subscribersMtx; std::mutex _clientsMtx; std::thread * _clientsRemoverThread = nullptr; std::atomic _stopRemoveClientsTask; void publishClientMsg(const Client & client, const char * msg, size_t msgSize); void publishClientDisconnected(const std::string&, const std::string&); pipe_ret_t waitForClient(uint32_t timeout); void clientEventHandler(const Client&, ClientEvent, const std::string &msg); void removeDeadClients(); void terminateDeadClientsRemover(); static pipe_ret_t sendToClient(const Client & client, const char * msg, size_t size); public: TcpServer(); ~TcpServer(); pipe_ret_t start(int port, int maxNumOfClients = 5, bool removeDeadClientsAutomatically = true); void initializeSocket(); void bindAddress(int port); void listenToClients(int maxNumOfClients); std::string acceptClient(uint timeout); void subscribe(const server_observer_t & observer); pipe_ret_t sendToAllClients(const char * msg, size_t size); pipe_ret_t sendToClient(const std::string & clientIP, const char * msg, size_t size); pipe_ret_t close(); void printClients(); }; ================================================ FILE: src/client.cpp ================================================ #include #include #include #include #include #include #include #include "../include/client.h" #include "../include/common.h" Client::Client(int fileDescriptor) { _sockfd.set(fileDescriptor); setConnected(false); } bool Client::operator==(const Client & other) const { if ((this->_sockfd.get() == other._sockfd.get()) && (this->_ip == other._ip) ) { return true; } return false; } void Client::startListen() { setConnected(true); _receiveThread = new std::thread(&Client::receiveTask, this); } void Client::send(const char *msg, size_t msgSize) const { const size_t numBytesSent = ::send(_sockfd.get(), (char *)msg, msgSize, 0); const bool sendFailed = (numBytesSent < 0); if (sendFailed) { throw std::runtime_error(strerror(errno)); } const bool notAllBytesWereSent = (numBytesSent < msgSize); if (notAllBytesWereSent) { char errorMsg[100]; sprintf(errorMsg, "Only %lu bytes out of %lu was sent to client", numBytesSent, msgSize); throw std::runtime_error(errorMsg); } } /* * Receive client packets, and notify user */ void Client::receiveTask() { while(isConnected()) { const fd_wait::Result waitResult = fd_wait::waitFor(_sockfd); if (waitResult == fd_wait::Result::FAILURE) { throw std::runtime_error(strerror(errno)); } else if (waitResult == fd_wait::Result::TIMEOUT) { continue; } char receivedMessage[MAX_PACKET_SIZE]; const size_t numOfBytesReceived = recv(_sockfd.get(), receivedMessage, MAX_PACKET_SIZE, 0); if(numOfBytesReceived < 1) { const bool clientClosedConnection = (numOfBytesReceived == 0); std::string disconnectionMessage; if (clientClosedConnection) { disconnectionMessage = "Client closed connection"; } else { disconnectionMessage = strerror(errno); } setConnected(false); publishEvent(ClientEvent::DISCONNECTED, disconnectionMessage); return; } else { publishEvent(ClientEvent::INCOMING_MSG, receivedMessage); } } } void Client::publishEvent(ClientEvent clientEvent, const std::string &msg) { _eventHandlerCallback(*this, clientEvent, msg); } void Client::print() const { const std::string connected = isConnected() ? "True" : "False"; std::cout << "-----------------\n" << "IP address: " << getIp() << std::endl << "Connected?: " << connected << std::endl << "Socket FD: " << _sockfd.get() << std::endl; } void Client::terminateReceiveThread() { setConnected(false); if (_receiveThread) { _receiveThread->join(); delete _receiveThread; _receiveThread = nullptr; } } void Client::close() { terminateReceiveThread(); const bool closeFailed = (::close(_sockfd.get()) == -1); if (closeFailed) { throw std::runtime_error(strerror(errno)); } } ================================================ FILE: src/common.cpp ================================================ #include #include "../include/file_descriptor.h" #include "../include/common.h" #include #define SELECT_FAILED -1 #define SELECT_TIMEOUT 0 namespace fd_wait { /** * monitor file descriptor and wait for I/O operation */ Result waitFor(const FileDescriptor &fileDescriptor, uint32_t timeoutSeconds) { struct timeval tv; tv.tv_sec = timeoutSeconds; tv.tv_usec = 0; fd_set fds; FD_ZERO(&fds); FD_SET(fileDescriptor.get(), &fds); const int selectRet = select(fileDescriptor.get() + 1, &fds, nullptr, nullptr, &tv); if (selectRet == SELECT_FAILED) { return Result::FAILURE; } else if (selectRet == SELECT_TIMEOUT) { return Result::TIMEOUT; } return Result::SUCCESS; } } ================================================ FILE: src/pipe_ret_t.cpp ================================================ #include #include "../include/pipe_ret_t.h" pipe_ret_t pipe_ret_t::failure(const std::string &msg) { return pipe_ret_t(false, msg); } pipe_ret_t pipe_ret_t::success(const std::string &msg) { return pipe_ret_t(true, msg); } ================================================ FILE: src/tcp_client.cpp ================================================ #include "../include/tcp_client.h" #include "../include/common.h" TcpClient::TcpClient() { _isConnected = false; _isClosed = true; } TcpClient::~TcpClient() { close(); } pipe_ret_t TcpClient::connectTo(const std::string & address, int port) { try { initializeSocket(); setAddress(address, port); } catch (const std::runtime_error& error) { return pipe_ret_t::failure(error.what()); } const int connectResult = connect(_sockfd.get() , (struct sockaddr *)&_server , sizeof(_server)); const bool connectionFailed = (connectResult == -1); if (connectionFailed) { return pipe_ret_t::failure(strerror(errno)); } startReceivingMessages(); _isConnected = true; _isClosed = false; return pipe_ret_t::success(); } void TcpClient::startReceivingMessages() { _receiveTask = new std::thread(&TcpClient::receiveTask, this); } void TcpClient::initializeSocket() { pipe_ret_t ret; _sockfd.set(socket(AF_INET , SOCK_STREAM , 0)); const bool socketFailed = (_sockfd.get() == -1); if (socketFailed) { throw std::runtime_error(strerror(errno)); } } void TcpClient::setAddress(const std::string& address, int port) { const int inetSuccess = inet_aton(address.c_str(), &_server.sin_addr); if(!inetSuccess) { // inet_addr failed to parse address // if hostname is not in IP strings and dots format, try resolve it struct hostent *host; struct in_addr **addrList; if ( (host = gethostbyname( address.c_str() ) ) == nullptr){ throw std::runtime_error("Failed to resolve hostname"); } addrList = (struct in_addr **) host->h_addr_list; _server.sin_addr = *addrList[0]; } _server.sin_family = AF_INET; _server.sin_port = htons(port); } pipe_ret_t TcpClient::sendMsg(const char * msg, size_t size) { const size_t numBytesSent = send(_sockfd.get(), msg, size, 0); if (numBytesSent < 0 ) { // send failed return pipe_ret_t::failure(strerror(errno)); } if (numBytesSent < size) { // not all bytes were sent char errorMsg[100]; sprintf(errorMsg, "Only %lu bytes out of %lu was sent to client", numBytesSent, size); return pipe_ret_t::failure(errorMsg); } return pipe_ret_t::success(); } void TcpClient::subscribe(const client_observer_t & observer) { std::lock_guard lock(_subscribersMtx); _subscibers.push_back(observer); } /* * Publish incomingPacketHandler client message to observer. * Observers get only messages that originated * from clients with IP address identical to * the specific observer requested IP */ void TcpClient::publishServerMsg(const char * msg, size_t msgSize) { std::lock_guard lock(_subscribersMtx); for (const auto &subscriber : _subscibers) { if (subscriber.incomingPacketHandler) { subscriber.incomingPacketHandler(msg, msgSize); } } } /* * Publish client disconnection to observer. * Observers get only notify about clients * with IP address identical to the specific * observer requested IP */ void TcpClient::publishServerDisconnected(const pipe_ret_t & ret) { std::lock_guard lock(_subscribersMtx); for (const auto &subscriber : _subscibers) { if (subscriber.disconnectionHandler) { subscriber.disconnectionHandler(ret); } } } /* * Receive server packets, and notify user */ void TcpClient::receiveTask() { while(_isConnected) { const fd_wait::Result waitResult = fd_wait::waitFor(_sockfd); if (waitResult == fd_wait::Result::FAILURE) { throw std::runtime_error(strerror(errno)); } else if (waitResult == fd_wait::Result::TIMEOUT) { continue; } char msg[MAX_PACKET_SIZE]; const size_t numOfBytesReceived = recv(_sockfd.get(), msg, MAX_PACKET_SIZE, 0); if(numOfBytesReceived < 1) { std::string errorMsg; if (numOfBytesReceived == 0) { //server closed connection errorMsg = "Server closed connection"; } else { errorMsg = strerror(errno); } _isConnected = false; publishServerDisconnected(pipe_ret_t::failure(errorMsg)); return; } else { publishServerMsg(msg, numOfBytesReceived); } } } void TcpClient::terminateReceiveThread() { _isConnected = false; if (_receiveTask) { _receiveTask->join(); delete _receiveTask; _receiveTask = nullptr; } } pipe_ret_t TcpClient::close(){ if (_isClosed) { return pipe_ret_t::failure("client is already closed"); } terminateReceiveThread(); const bool closeFailed = (::close(_sockfd.get()) == -1); if (closeFailed) { return pipe_ret_t::failure(strerror(errno)); } _isClosed = true; return pipe_ret_t::success(); } ================================================ FILE: src/tcp_server.cpp ================================================ #include #include #include #include "../include/tcp_server.h" #include "../include/common.h" TcpServer::TcpServer() { _subscribers.reserve(10); _clients.reserve(10); _stopRemoveClientsTask = false; } TcpServer::~TcpServer() { close(); } void TcpServer::subscribe(const server_observer_t & observer) { std::lock_guard lock(_subscribersMtx); _subscribers.push_back(observer); } void TcpServer::printClients() { std::lock_guard lock(_clientsMtx); if (_clients.empty()) { std::cout << "no connected clients\n"; } for (const Client *client : _clients) { client->print(); } } /** * Remove dead clients (disconnected) from clients vector periodically */ void TcpServer::removeDeadClients() { std::vector::const_iterator clientToRemove; while (!_stopRemoveClientsTask) { { std::lock_guard lock(_clientsMtx); do { clientToRemove = std::find_if(_clients.begin(), _clients.end(), [](Client *client) { return !client->isConnected(); }); if (clientToRemove != _clients.end()) { (*clientToRemove)->close(); delete *clientToRemove; _clients.erase(clientToRemove); } } while (clientToRemove != _clients.end()); } sleep(2); } } void TcpServer::terminateDeadClientsRemover() { if (_clientsRemoverThread) { _stopRemoveClientsTask = true; _clientsRemoverThread->join(); delete _clientsRemoverThread; _clientsRemoverThread = nullptr; } } /** * Handle different client events. Subscriber callbacks should be short and fast, and must not * call other server functions to avoid deadlock */ void TcpServer::clientEventHandler(const Client &client, ClientEvent event, const std::string &msg) { switch (event) { case ClientEvent::DISCONNECTED: { publishClientDisconnected(client.getIp(), msg); break; } case ClientEvent::INCOMING_MSG: { publishClientMsg(client, msg.c_str(), msg.size()); break; } } } /* * Publish incomingPacketHandler client message to observer. * Observers get only messages that originated * from clients with IP address identical to * the specific observer requested IP */ void TcpServer::publishClientMsg(const Client & client, const char * msg, size_t msgSize) { std::lock_guard lock(_subscribersMtx); for (const server_observer_t& subscriber : _subscribers) { if (subscriber.wantedIP == client.getIp() || subscriber.wantedIP.empty()) { if (subscriber.incomingPacketHandler) { subscriber.incomingPacketHandler(client.getIp(), msg, msgSize); } } } } /* * Publish client disconnection to observer. * Observers get only notify about clients * with IP address identical to the specific * observer requested IP */ void TcpServer::publishClientDisconnected(const std::string &clientIP, const std::string &clientMsg) { std::lock_guard lock(_subscribersMtx); for (const server_observer_t& subscriber : _subscribers) { if (subscriber.wantedIP == clientIP) { if (subscriber.disconnectionHandler) { subscriber.disconnectionHandler(clientIP, clientMsg); } } } } /* * Bind port and start listening * Return tcp_ret_t */ pipe_ret_t TcpServer::start(int port, int maxNumOfClients, bool removeDeadClientsAutomatically) { if (removeDeadClientsAutomatically) { _clientsRemoverThread = new std::thread(&TcpServer::removeDeadClients, this); } try { initializeSocket(); bindAddress(port); listenToClients(maxNumOfClients); } catch (const std::runtime_error &error) { return pipe_ret_t::failure(error.what()); } return pipe_ret_t::success(); } void TcpServer::initializeSocket() { _sockfd.set(socket(AF_INET, SOCK_STREAM, 0)); const bool socketFailed = (_sockfd.get() == -1); if (socketFailed) { throw std::runtime_error(strerror(errno)); } // set socket for reuse (otherwise might have to wait 4 minutes every time socket is closed) const int option = 1; setsockopt(_sockfd.get(), SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)); } void TcpServer::bindAddress(int port) { memset(&_serverAddress, 0, sizeof(_serverAddress)); _serverAddress.sin_family = AF_INET; _serverAddress.sin_addr.s_addr = htonl(INADDR_ANY); _serverAddress.sin_port = htons(port); const int bindResult = bind(_sockfd.get(), (struct sockaddr *)&_serverAddress, sizeof(_serverAddress)); const bool bindFailed = (bindResult == -1); if (bindFailed) { throw std::runtime_error(strerror(errno)); } } void TcpServer::listenToClients(int maxNumOfClients) { const int clientsQueueSize = maxNumOfClients; const bool listenFailed = (listen(_sockfd.get(), clientsQueueSize) == -1); if (listenFailed) { throw std::runtime_error(strerror(errno)); } } /* * Accept and handle new client socket. To handle multiple clients, user must * call this function in a loop to enable the acceptance of more than one. * If timeout argument equal 0, this function is executed in blocking mode. * If timeout argument is > 0 then this function is executed in non-blocking * mode (async) and will quit after timeout seconds if no client tried to connect. * Return accepted client IP, or throw error if failed */ std::string TcpServer::acceptClient(uint timeout) { const pipe_ret_t waitingForClient = waitForClient(timeout); if (!waitingForClient.isSuccessful()) { throw std::runtime_error(waitingForClient.message()); } socklen_t socketSize = sizeof(_clientAddress); const int fileDescriptor = accept(_sockfd.get(), (struct sockaddr*)&_clientAddress, &socketSize); const bool acceptFailed = (fileDescriptor == -1); if (acceptFailed) { throw std::runtime_error(strerror(errno)); } auto newClient = new Client(fileDescriptor); newClient->setIp(inet_ntoa(_clientAddress.sin_addr)); using namespace std::placeholders; newClient->setEventsHandler(std::bind(&TcpServer::clientEventHandler, this, _1, _2, _3)); newClient->startListen(); std::lock_guard lock(_clientsMtx); _clients.push_back(newClient); return newClient->getIp(); } pipe_ret_t TcpServer::waitForClient(uint32_t timeout) { if (timeout > 0) { const fd_wait::Result waitResult = fd_wait::waitFor(_sockfd, timeout); const bool noIncomingClient = (!FD_ISSET(_sockfd.get(), &_fds)); if (waitResult == fd_wait::Result::FAILURE) { return pipe_ret_t::failure(strerror(errno)); } else if (waitResult == fd_wait::Result::TIMEOUT) { return pipe_ret_t::failure("Timeout waiting for client"); } else if (noIncomingClient) { return pipe_ret_t::failure("File descriptor is not set"); } } return pipe_ret_t::success(); } /* * Send message to all connected clients. * Return true if message was sent successfully to all clients */ pipe_ret_t TcpServer::sendToAllClients(const char * msg, size_t size) { std::lock_guard lock(_clientsMtx); for (const Client *client : _clients) { pipe_ret_t sendingResult = sendToClient(*client, msg, size); if (!sendingResult.isSuccessful()) { return sendingResult; } } return pipe_ret_t::success(); } /* * Send message to specific client (determined by client IP address). * Return true if message was sent successfully */ pipe_ret_t TcpServer::sendToClient(const Client & client, const char * msg, size_t size){ try{ client.send(msg, size); } catch (const std::runtime_error &error) { return pipe_ret_t::failure(error.what()); } return pipe_ret_t::success(); } pipe_ret_t TcpServer::sendToClient(const std::string & clientIP, const char * msg, size_t size) { std::lock_guard lock(_clientsMtx); const auto clientIter = std::find_if(_clients.begin(), _clients.end(), [&clientIP](Client *client) { return client->getIp() == clientIP; }); if (clientIter == _clients.end()) { return pipe_ret_t::failure("client not found"); } const Client &client = *(*clientIter); return sendToClient(client, msg, size); } /* * Close server and clients resources. * Return true is successFlag, false otherwise */ pipe_ret_t TcpServer::close() { terminateDeadClientsRemover(); { // close clients std::lock_guard lock(_clientsMtx); for (Client * client : _clients) { try { client->close(); } catch (const std::runtime_error& error) { return pipe_ret_t::failure(error.what()); } } _clients.clear(); } { // close server const int closeServerResult = ::close(_sockfd.get()); const bool closeServerFailed = (closeServerResult == -1); if (closeServerFailed) { return pipe_ret_t::failure(strerror(errno)); } } return pipe_ret_t::success(); }