Решил задаться целью написать простой в использовании и при этом быстрый многопоточного TCP/IP сервера на C++ и при этом кроссплатформенный — как минимум чтобы работал на платформах Windows и Linux без требования как-либо изменять код за пределами самописной библиотеки. Ранее, на чистом C++ без библиотек вроде Qt, сетевым программировнием не занимался, и предвещал себе долгое время мучений с платформо-зависимостью. Но как оказалось всё гораздо проще чем казалось на первый взгляд, ведь в основном интерфейсы сокетов обоих систем похожи как две капли воды и различаются лишь в мелких деталях.
Для начала определим общий для клиента и сервера заголовок:
general.h
#ifndef GENERAL_H #define GENERAL_H #ifdef _WIN32 #else #define SD_BOTH 0 #endif #include <cstdint> #include <cstring> #include <cinttypes> #include <malloc.h> // IP 127.0.0.1 uint32_t LOCALHOST_IP = 0x0100007f; // Код состояния сокета enum class SocketStatus : uint8_t { connected = 0, err_socket_init = 1, err_socket_bind = 2, err_socket_connect = 3, disconnected = 4 }; // Буффер данных куда у нас будет приниматься данные от другой стороны struct DataBuffer { int size = 0; void* data_ptr = nullptr; DataBuffer() = default; DataBuffer(int size, void* data_ptr) : size(size), data_ptr(data_ptr) {} DataBuffer(const DataBuffer& other) : size(other.size), data_ptr(malloc(size)) {memcpy(data_ptr, other.data_ptr, size);} DataBuffer(DataBuffer&& other) : size(other.size), data_ptr(other.data_ptr) {other.data_ptr = nullptr;} ~DataBuffer() {if(data_ptr) free(data_ptr); data_ptr = nullptr;} bool isEmpty() {return !data_ptr || !size;} operator bool() {return data_ptr && size;} }; // в последней версии библиотеки typedef от std::vector<uint8_t> // Тип сокета enum class SocketType : uint8_t { client_socket = 0, server_socket = 1 }; // Базовый класс TCP клиента class TcpClientBase { public: typedef SocketStatus status; virtual ~TcpClientBase() {}; virtual status disconnect() = 0; virtual status getStatus() const = 0; virtual bool sendData(const void* buffer, const size_t size) const = 0; virtual DataBuffer loadData() = 0; virtual uint32_t getHost() const = 0; virtual uint16_t getPort() const = 0; virtual SocketType getType() const = 0; }; #endif // GENERAL_H
Итак интерфейсы классов сервера и клиента(со стороны сервера) выглядят следующим образом:
TcpServer.h
#ifndef TCPSERVER_H #define TCPSERVER_H #include <functional> #include <list> #include <thread> #include <mutex> #include <shared_mutex> #ifdef _WIN32 // Windows NT #include <WinSock2.h> #include <mstcpip.h> #else // *nix #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #endif #include "general.h" #ifdef _WIN32 // Windows NT typedef int SockLen_t; typedef SOCKADDR_IN SocketAddr_in; typedef SOCKET Socket; typedef u_long ka_prop_t; #else // POSIX typedef socklen_t SockLen_t; typedef struct sockaddr_in SocketAddr_in; typedef int Socket; typedef int ka_prop_t; #endif // Конфигурация Keep-Alive соединения struct KeepAliveConfig{ ka_prop_t ka_idle = 120; ka_prop_t ka_intvl = 3; ka_prop_t ka_cnt = 5; }; // Класс Tcp сервера struct TcpServer { // Класс клиента сервера (реализация определена ниже) struct Client; // Тип обработчик данных клиента typedef std::function<void(DataBuffer, Client&)> handler_function_t; // Тип обработчика подключения/отсоединения клиента typedef std::function<void(Client&)> con_handler_function_t; // Коды статуса сервера enum class status : uint8_t { up = 0, err_socket_init = 1, err_socket_bind = 2, err_scoket_keep_alive = 3, err_socket_listening = 4, close = 5 }; private: // Сокет сервера Socket serv_socket; // Порт сервера uint16_t port; // Код статуса status _status = status::close; // Обработчик данных от клиента handler_function_t handler; // Обработчик подключения клиента con_handler_function_t connect_hndl = [](Client&){}; // Обработчик отсоединения клиента con_handler_function_t disconnect_hndl = [](Client&){}; // Поток-обработчик подключений std::thread accept_handler_thread; // Поток ожидания данных std::thread data_waiter_thread; // Тип итератора клиента typedef std::list<std::unique_ptr<Client>>::iterator ClientIterator; // Keep-Alive конфигурация KeepAliveConfig ka_conf; // Список клиентов std::list<std::unique_ptr<Client>> client_list; // Мьютекс для синзронизации потоков подключения и ожидания данных std::mutex client_mutex; // Для систем Windows так же требуется // структура определяющая версию WinSocket #ifdef _WIN32 // Windows NT WSAData w_data; #endif // Включить Keep-Alive для сокета bool enableKeepAlive(Socket socket); // Метод обработчика подключений void handlingAcceptLoop(); // Метод ожидания данных void waitingDataLoop(); public: // Упрощённый конструктор с указанием: // * порта // * обработчика данных // * конфигурации Keep-Alive TcpServer(const uint16_t port, handler_function_t handler, KeepAliveConfig ka_conf = {}); // Конструктор с указанием: // * порта // * обработчика данных // * обработчика подключений // * обработчика отключений // * конфигурации Keep-Alive TcpServer(const uint16_t port, handler_function_t handler, con_handler_function_t connect_hndl, con_handler_function_t disconnect_hndl, KeepAliveConfig ka_conf = {}); // Деструктор ~TcpServer(); // Заменить обработчик данных void setHandler(handler_function_t handler); // Getter порта uint16_t getPort() const; // Setter порта uint16_t setPort(const uint16_t port); // Getter кода статуса сервера status getStatus() const {return _status;} // Метод запуска сервера status start(); // Метод остановки сервера void stop(); // Метод для входа присоединения циклических потоков сервера void joinLoop(); // Исходящее подключение от сервера к другому серверу bool connectTo(uint32_t host, uint16_t port, con_handler_function_t connect_hndl); // Отправить данные всем клиентам сервера void sendData(const void* buffer, const size_t size); // Отправить данные клиенту по порту и хосту bool sendDataBy(uint32_t host, uint16_t port, const void* buffer, const size_t size); // Отключить клиента по порту и хосту bool disconnectBy(uint32_t host, uint16_t port); // Отключить всех клиентов void disconnectAll(); }; // Класс клиента (со стороны сервера) struct TcpServer::Client : public TcpClientBase { friend struct TcpServer; // Мьютекс для синхронизации обработки данныз std::mutex access_mtx; // Адрес клиента SocketAddr_in address; // Сокет слиента Socket socket; // Код статуса клиента status _status = status::connected; public: // Конструктор с указанием: // * сокета клиента // * адреса клиента Client(Socket socket, SocketAddr_in address); // Деструктор virtual ~Client() override; // Getter хоста virtual uint32_t getHost() const override; // Getter порта virtual uint16_t getPort() const override; // Getter кода статуса подключения virtual status getStatus() const override {return _status;} // Отключить клиента virtual status disconnect() override; // Получить данные от клиента virtual DataBuffer loadData() override; // Отправить данные клиенту virtual bool sendData(const void* buffer, const size_t size) const override; // Определить "сторону" клиента virtual SocketType getType() const override {return SocketType::server_socket;} }; #endif // TCPSERVER_H
Как можно заметить на данном этапи зависимости операционных систем без особых проблем решаются при помощи макросов и псевдонимов типов данных. Так же в Windows части TcpServer-хедера присутствует структура для обозначения используемой версии WinSocket — WSAData w_data;(см. WSAData)
Перейдём к реализации сервера:
TcpServer.cpp
#include "../include/TcpServer.h" #include <chrono> #include <cstring> #include <mutex> #ifdef _WIN32 // Макросы для выражений зависимых от OS #define WIN(exp) exp #define NIX(exp) // Конвертировать WinSocket код ошибки в Posix код ошибки inline int convertError() { switch (WSAGetLastError()) { case 0: return 0; case WSAEINTR: return EINTR; case WSAEINVAL: return EINVAL; case WSA_INVALID_HANDLE: return EBADF; case WSA_NOT_ENOUGH_MEMORY: return ENOMEM; case WSA_INVALID_PARAMETER: return EINVAL; case WSAENAMETOOLONG: return ENAMETOOLONG; case WSAENOTEMPTY: return ENOTEMPTY; case WSAEWOULDBLOCK: return EAGAIN; case WSAEINPROGRESS: return EINPROGRESS; case WSAEALREADY: return EALREADY; case WSAENOTSOCK: return ENOTSOCK; case WSAEDESTADDRREQ: return EDESTADDRREQ; case WSAEMSGSIZE: return EMSGSIZE; case WSAEPROTOTYPE: return EPROTOTYPE; case WSAENOPROTOOPT: return ENOPROTOOPT; case WSAEPROTONOSUPPORT: return EPROTONOSUPPORT; case WSAEOPNOTSUPP: return EOPNOTSUPP; case WSAEAFNOSUPPORT: return EAFNOSUPPORT; case WSAEADDRINUSE: return EADDRINUSE; case WSAEADDRNOTAVAIL: return EADDRNOTAVAIL; case WSAENETDOWN: return ENETDOWN; case WSAENETUNREACH: return ENETUNREACH; case WSAENETRESET: return ENETRESET; case WSAECONNABORTED: return ECONNABORTED; case WSAECONNRESET: return ECONNRESET; case WSAENOBUFS: return ENOBUFS; case WSAEISCONN: return EISCONN; case WSAENOTCONN: return ENOTCONN; case WSAETIMEDOUT: return ETIMEDOUT; case WSAECONNREFUSED: return ECONNREFUSED; case WSAELOOP: return ELOOP; case WSAEHOSTUNREACH: return EHOSTUNREACH; default: return EIO; } } #else // Макросы для выражений зависимых от OS #define WIN(exp) #define NIX(exp) exp #endif // Реализация конструктора сервера с указанием // * порта // * обработчика данных // * Keep-Alive конфигурации TcpServer::TcpServer(const uint16_t port, handler_function_t handler, KeepAliveConfig ka_conf) : TcpServer(port, handler, [](Client&){}, [](Client&){}, ka_conf) {} // Реализация конструктора сервера с указанием // * порта // * обработчика данных // * обработчика подключения // * обработчика отключения // * Keep-Alive конфигурации TcpServer::TcpServer(const uint16_t port, handler_function_t handler, con_handler_function_t connect_hndl, con_handler_function_t disconnect_hndl, KeepAliveConfig ka_conf) : port(port), handler(handler), connect_hndl(connect_hndl), disconnect_hndl(disconnect_hndl), ka_conf(ka_conf) {} // Деструктор сервера // автоматически закрывает сокет сервера TcpServer::~TcpServer() { if(_status == status::up) stop(); WIN(WSACleanup()); } // Setter обработчика данных void TcpServer::setHandler(TcpServer::handler_function_t handler) {this->handler = handler;} // Getter порта uint16_t TcpServer::getPort() const {return port;} // Setter порта uint16_t TcpServer::setPort( const uint16_t port) { this->port = port; start(); return port; } // Реализация запуска сервера TcpServer::status TcpServer::start() { int flag; // Если сервер запущен, то отключаем его if(_status == status::up) stop(); // Для Windows указываем версию WinSocket WIN(if(WSAStartup(MAKEWORD(2, 2), &w_data) == 0) {}) // Задаём адрес сервера SocketAddr_in address; // INADDR_ANY - любой IP адрес address.sin_addr WIN(.S_un.S_addr)NIX(.s_addr) = INADDR_ANY; // Задаём порт сервера address.sin_port = htons(port); // Семейство сети AF_INET - IPv4 (AF_INET6 - IPv6) address.sin_family = AF_INET; // Создаём TCP сокет if((serv_socket = socket(AF_INET, SOCK_STREAM, 0)) WIN(== INVALID_SOCKET)NIX(== -1)) return _status = status::err_socket_init; flag = true; // Устанавливаем параметр сокета SO_REUSEADDR в true (подробнее https://it.wikireading.ru/7093) if((setsockopt(serv_socket, SOL_SOCKET, SO_REUSEADDR, WIN((char*))&flag, sizeof(flag)) == -1) || // Привязываем к сокету адрес и порт (bind(serv_socket, (struct sockaddr*)&address, sizeof(address)) WIN(== SOCKET_ERROR)NIX(< 0))) return _status = status::err_socket_bind; // Активируем ожидание фходящих соединений if(listen(serv_socket, SOMAXCONN) WIN(== SOCKET_ERROR)NIX(< 0)) return _status = status::err_socket_listening; _status = status::up; // Запускаем поток ожидания соединений accept_handler_thread = std::thread([this]{handlingAcceptLoop();}); // Запускаем поток ожидания данных data_waiter_thread = std::thread([this]{waitingDataLoop();}); return _status; } // Реализация остановки сервера void TcpServer::stop() { _status = status::close; // Закрываем сокет WIN(closesocket)NIX(close)(serv_socket); // Ожидаем завершения потоков joinLoop(); // Вычищаем список клиентов client_list.clear(); } // "Вхождение" в потоки ожидания void TcpServer::joinLoop() {accept_handler_thread.join(); data_waiter_thread.join();} // Создание подключение со стороны сервера // (подключение аналогично клиентоскому, но обрабатывается // тем же обработчиком, что и входящие соединения) bool TcpServer::connectTo(uint32_t host, uint16_t port, con_handler_function_t connect_hndl) { Socket client_socket; SocketAddr_in address; // Создание TCP сокета if((client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP)) WIN(== INVALID_SOCKET) NIX(< 0)) return false; new(&address) SocketAddr_in; address.sin_family = AF_INET; address.sin_addr.s_addr = host; WIN(address.sin_addr.S_un.S_addr = host;) NIX(address.sin_addr.s_addr = host;) address.sin_port = htons(port); // Установка соединения if(connect(client_socket, (sockaddr *)&address, sizeof(address)) WIN(== SOCKET_ERROR)NIX(!= 0) ) { WIN(closesocket(client_socket);)NIX(close(client_socket);) return false; } // Активация Keep-Alive if(!enableKeepAlive(client_socket)) { shutdown(client_socket, 0); WIN(closesocket)NIX(close)(client_socket) } std::unique_ptr<Client> client(new Client(client_socket, address)); // Запуск обработчика подключения connect_hndl(*client); // Добавление клиента в список клиентов client_mutex.lock(); client_list.emplace_back(std::move(client)); client_mutex.unlock(); return true; } // Отправка данных всем клиентам void TcpServer::sendData(const void* buffer, const size_t size) { for(std::unique_ptr<Client>& client : client_list) client->sendData(buffer, size); } // Отправка данных по конкретному хосту и порту bool TcpServer::sendDataBy(uint32_t host, uint16_t port, const void* buffer, const size_t size) { bool data_is_sended = false; for(std::unique_ptr<Client>& client : client_list) if(client->getHost() == host && client->getPort() == port) { client->sendData(buffer, size); data_is_sended = true; } return data_is_sended; } // Отключение клиента по конкретному хосту и порту bool TcpServer::disconnectBy(uint32_t host, uint16_t port) { bool client_is_disconnected = false; for(std::unique_ptr<Client>& client : client_list) if(client->getHost() == host && client->getPort() == port) { client->disconnect(); client_is_disconnected = true; } return client_is_disconnected; } // Отключение всех клиентов void TcpServer::disconnectAll() { for(std::unique_ptr<Client>& client : client_list) client->disconnect(); } // Цикл обработки входящих подключений // (исполняется в отдельном потоке) void TcpServer::handlingAcceptLoop() { SockLen_t addrlen = sizeof(SocketAddr_in); // Пока сервер запущен while (_status == status::up) { SocketAddr_in client_addr; // Принятеи новго подключения (блокирующи вызов) if (Socket client_socket = accept(serv_socket, (struct sockaddr*)&client_addr, &addrlen); client_socket WIN(!= 0)NIX(>= 0) && _status == status::up) { // Если получен сокет с ошибкой продолжить ожидание if(client_socket == WIN(INVALID_SOCKET)NIX(-1)) continue; // Активировать Keep-Alive для клиента if(!enableKeepAlive(client_socket)) { shutdown(client_socket, 0); WIN(closesocket)NIX(close)(client_socket); } std::unique_ptr<Client> client(new Client(client_socket, client_addr)); // Запустить обработчик подключений connect_hndl(*client); // Добавить клиента в список клиентов client_mutex.lock(); client_list.emplace_back(std::move(client)); client_mutex.unlock(); } } } // Цикл ожидания данных void TcpServer::waitingDataLoop() { using namespace std::chrono_literals; while (true) { client_mutex.lock(); // Перебрать всех клиентов for(auto it = client_list.begin(), end = client_list.end(); it != end; ++it) { auto& client = *it; // Если unique_ptr содержит объект клиента if(client){ if(DataBuffer data = client->loadData(); data.size) { // При наличии данных запустить обработку входящих данных в отдельном потоке std::thread([this, _data = std::move(data), &client]{ client->access_mtx.lock(); handler(_data, *client); client->access_mtx.unlock(); }).detach(); } else if(client->_status == SocketStatus::disconnected) { // При отключении клиента запустить обработку в отдельном потоке std::thread([this, &client, it]{ // Извлечь объект клиента из unique_ptr в списке client->access_mtx.lock(); Client* pointer = client.release(); client = nullptr; pointer->access_mtx.unlock(); // Запуск обработчика отключения disconnect_hndl(*pointer); // Удалить элемент клиента из списка client_list.erase(it); // Удалить объект клиента delete pointer; }).detach(); } } } client_mutex.unlock(); // Ожидание 50 млисекунд так как в данном потоке // не содержится блокирующих вызовов и данный // цикл сильно повышает загруженность CPU std::this_thread::sleep_for(50ms); } } // Функция запуска и конфигурации Keep-Alive для сокета bool TcpServer::enableKeepAlive(Socket socket) { int flag = 1; #ifdef _WIN32 tcp_keepalive ka {1, ka_conf.ka_idle * 1000, ka_conf.ka_intvl * 1000}; if (setsockopt (socket, SOL_SOCKET, SO_KEEPALIVE, (const char *) &flag, sizeof(flag)) != 0) return false; unsigned long numBytesReturned = 0; if(WSAIoctl(socket, SIO_KEEPALIVE_VALS, &ka, sizeof (ka), nullptr, 0, &numBytesReturned, 0, nullptr) != 0) return false; #else //POSIX if(setsockopt(socket, SOL_SOCKET, SO_KEEPALIVE, &flag, sizeof(flag)) == -1) return false; if(setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE, &ka_conf.ka_idle, sizeof(ka_conf.ka_idle)) == -1) return false; if(setsockopt(socket, IPPROTO_TCP, TCP_KEEPINTVL, &ka_conf.ka_intvl, sizeof(ka_conf.ka_intvl)) == -1) return false; if(setsockopt(socket, IPPROTO_TCP, TCP_KEEPCNT, &ka_conf.ka_cnt, sizeof(ka_conf.ka_cnt)) == -1) return false; #endif return true; }
Реализация для Linux и Windows практически идентична за исключением некотрых мест который без проблем обкладываются макросами. Теперь же мы перейдём непосредственно к реализации класса клиента:
TcpServerClient.cpp
#include "../include/TcpServer.h" #ifdef _WIN32 // Макросы для выражений зависимых от OS #define WIN(exp) exp #define NIX(exp) // Конвертировать WinSocket код ошибки в Posix код ошибки inline int convertError() { switch (WSAGetLastError()) { case 0: return 0; case WSAEINTR: return EINTR; case WSAEINVAL: return EINVAL; case WSA_INVALID_HANDLE: return EBADF; case WSA_NOT_ENOUGH_MEMORY: return ENOMEM; case WSA_INVALID_PARAMETER: return EINVAL; case WSAENAMETOOLONG: return ENAMETOOLONG; case WSAENOTEMPTY: return ENOTEMPTY; case WSAEWOULDBLOCK: return EAGAIN; case WSAEINPROGRESS: return EINPROGRESS; case WSAEALREADY: return EALREADY; case WSAENOTSOCK: return ENOTSOCK; case WSAEDESTADDRREQ: return EDESTADDRREQ; case WSAEMSGSIZE: return EMSGSIZE; case WSAEPROTOTYPE: return EPROTOTYPE; case WSAENOPROTOOPT: return ENOPROTOOPT; case WSAEPROTONOSUPPORT: return EPROTONOSUPPORT; case WSAEOPNOTSUPP: return EOPNOTSUPP; case WSAEAFNOSUPPORT: return EAFNOSUPPORT; case WSAEADDRINUSE: return EADDRINUSE; case WSAEADDRNOTAVAIL: return EADDRNOTAVAIL; case WSAENETDOWN: return ENETDOWN; case WSAENETUNREACH: return ENETUNREACH; case WSAENETRESET: return ENETRESET; case WSAECONNABORTED: return ECONNABORTED; case WSAECONNRESET: return ECONNRESET; case WSAENOBUFS: return ENOBUFS; case WSAEISCONN: return EISCONN; case WSAENOTCONN: return ENOTCONN; case WSAETIMEDOUT: return ETIMEDOUT; case WSAECONNREFUSED: return ECONNREFUSED; case WSAELOOP: return ELOOP; case WSAEHOSTUNREACH: return EHOSTUNREACH; default: return EIO; } } #else // Макросы для выражений зависимых от OS #define WIN(exp) #define NIX(exp) exp #endif #include <iostream> // Реализация загрузки данных DataBuffer TcpServer::Client::loadData() { // Если клиент не подключён вернуть пустой буффер if(_status != SocketStatus::connected) return DataBuffer(); using namespace std::chrono_literals; DataBuffer buffer; int err; // Чтение длинный данных в неблокирующем режиме // MSG_DONTWAIT - Unix флаг неблокирующего режима для recv // FIONBIO - Windows-флаг неблокирующего режима для ioctlsocket WIN(if(u_long t = true; SOCKET_ERROR == ioctlsocket(socket, FIONBIO, &t)) return DataBuffer();) int answ = recv(socket, (char*)&buffer.size, sizeof (buffer.size), NIX(MSG_DONTWAIT)WIN(0)); // Обработка отключения if(!answ) { disconnect(); return DataBuffer(); } else if(answ == -1) { // Чтение кода ошибки WIN( err = convertError(); if(!err) { SockLen_t len = sizeof (err); getsockopt (socket, SOL_SOCKET, SO_ERROR, WIN((char*))&err, &len); } )NIX( SockLen_t len = sizeof (err); getsockopt (socket, SOL_SOCKET, SO_ERROR, WIN((char*))&err, &len); if(!err) err = errno; ) // Отключение неблокирующего режима для Windows WIN(if(u_long t = false; SOCKET_ERROR == ioctlsocket(socket, FIONBIO, &t)) return DataBuffer();) // Обработка ошибки при наличии switch (err) { case 0: break; // Keep alive timeout case ETIMEDOUT: case ECONNRESET: case EPIPE: disconnect(); [[fallthrough]]; // No data case EAGAIN: return DataBuffer(); default: disconnect(); std::cerr << "Unhandled error!\n" << "Code: " << err << " Err: " << std::strerror(err) << '\n'; return DataBuffer(); } } // Если прочитанный размер нулевой, то вернуть пустой буффер if(!buffer.size) return DataBuffer(); // Если размер не нулевой выделить буффер в куче для чтения данных buffer.data_ptr = (char*)malloc(buffer.size); // Чтение данных в блокирующем режиме recv(socket, (char*)buffer.data_ptr, buffer.size, 0); // Возврат буффера с прочитанными данными return buffer; } // Обработка отключения клиента TcpClientBase::status TcpServer::Client::disconnect() { _status = status::disconnected; // Если сокет не валидный прекратить обработку if(socket == WIN(INVALID_SOCKET)NIX(-1)) return _status; // Отключение сокета shutdown(socket, SD_BOTH) // Закрытие сокета WIN(closesocket)NIX(close)(socket); // Установление в сокета не валидного значения socket = WIN(INVALID_SOCKET)NIX(-1); return _status; } // Отправка данных bool TcpServer::Client::sendData(const void* buffer, const size_t size) const { // Если сокет закрыт вернуть false if(_status != SocketStatus::connected) return false; // Сформировать сообщение с длинной в начале void* send_buffer = malloc(size + sizeof (int)); memcpy(reinterpret_cast<char*>(send_buffer) + sizeof(int), buffer, size); *reinterpret_cast<int*>(send_buffer) = size; // Отправить сообщение if(send(socket, reinterpret_cast<char*>(send_buffer), size + sizeof (int), 0) < 0) return false; // Вычистить буффер сообщения free(send_buffer); return true; } // Конструктор клиента TcpServer::Client::Client(Socket socket, SocketAddr_in address) : address(address), socket(socket) {} // Деструктор клиента с закрытием сокета TcpServer::Client::~Client() { if(socket == WIN(INVALID_SOCKET)NIX(-1)) return; shutdown(socket, SD_BOTH); WIN(closesocket(socket);) NIX(close(socket);) } // Получить хост клиента uint32_t TcpServer::Client::getHost() const {return NIX(address.sin_addr.s_addr) WIN(address.sin_addr.S_un.S_addr);} // Получить порт клиента uint16_t TcpServer::Client::getPort() const {return address.sin_port;}
Пример использования:
main.cpp
#include <iostream> // Парсинр IPv4-адреса и порта в std::string std::string getHostStr(const TcpServer::Client& client) { uint32_t ip = client.getHost (); return std::string() + std::to_string(int(reinterpret_cast<char*>(&ip)[0])) + '.' + std::to_string(int(reinterpret_cast<char*>(&ip)[1])) + '.' + std::to_string(int(reinterpret_cast<char*>(&ip)[2])) + '.' + std::to_string(int(reinterpret_cast<char*>(&ip)[3])) + ':' + std::to_string( client.getPort ()); } int main() { // Создание экземпляра сервера TcpServer server( 8080, [](DataBuffer data, TcpServer::Client& client){ std::cout << "("<<getHostStr(client)<<")[ " << data.size << " bytes ]: " << (char*)data.data_ptr << '\n'; client.sendData("Hello, client!", sizeof("Hello, client!")); }, {1, 1, 1}); // Keep alive{ожидание:1s, интервал: 1s, кол-во пакетов: 1}; // Запуск сервера if(server.start() == TcpServer::status::up) { std::cout << "Server is up!" << std::endl; server.joinLoop(); //Joing to the client handling loop } else { std::cout << "Server start error! Error code:" << int(server.getStatus()) << std::endl; return -1; } }
Источники:
- Программирование сокетов в Linux
- Простой и быстрый сервер на C/C++...
- Winsock Server Application
- Проект OpenNet: MAN socket (7) Макропакеты и соглашения (FreeBSD и Linux)
UPDATE: Реализация многопоточного TCP-сервера представленная в данной статье имеет такой недостаток как "потокове голодание" при слишком большом колличестве подключённых к серверу клиентов, по скольку для обработки каждого клиента создаётся отдельный поток, который находится в состоянии активного ожидания данных от клиента. Для уменьшения влияния данного недостатка было принято решение изменить модель организации многопоточности приложения. Новая модель представлена на следующей схеме:

Как можно заметить, при данной модели организации многопоточности имеется всего один поток-обходчик, который проходит все клиенты и проверяет наличие пришедших от них данных. При условии найденых данных поток-обходчик переходит в состояние обработки данных клиента предварительно создав новый поток-обходчик, который идёт дальше. После обработки данных поток-обработчик переносит объект клиента в конец очереди ожидания и самоуничтожается.
Реализацию с применением данного исправления можно увидеть в данном репозитории GitHub. Данный сервер рассчитан только для отправки бинарных данных по протоколу TCP, поскольку отправляет вместе с данными заголовок с их размером в байтах, так что для реализации HTTP-сервера данная реализация не подходит. Реализованно это именно так по причине того что TCP/IP — потоковый протокол, то есть данные идущие через TCP/IP никак не терминируются, что осложняет работу именно в случаях работы с сырыми данными, когда поток нельзя терминировать "завершающим символом", как это принято в текстовых потоках.
UPDATE 2: На текущий момент, реализация представленная в репозитории GitHub лишена недостатка с "потоковым голоданием". Операции чтения префикса размера бинарного пакета при получении его от клиента, а так же приём новых соединений переведены в неблокирующий режим, а многопоточность реализована с помощью простой реализации пула потоков.
