Простая, современная библиотека сокетов C++.
Это довольно низкоуровневая оболочка C++ вокруг библиотеки сокетов Беркли, использующая классы socket
, acceptor,
и connector
, которые являются знакомыми концепциями из других языков.
Базовый класс socket
оборачивает дескриптор системного сокета и сохраняет его время жизни. Когда объект C++ выходит за пределы области видимости, он закрывает базовый дескриптор сокета. Объекты сокетов обычно можно перемещать , но нельзя копировать . Сокет можно перенести из одной области действия (или потока) в другую с помощью std::move()
.
В настоящее время библиотека поддерживает: IPv4 и IPv6 в Linux, Mac и Windows. Другие системы *nix и POSIX должны работать с небольшими изменениями или без них.
Сокеты Unix-Domain доступны в системах *nix, в которых есть реализация ОС.
Недавно была добавлена базовая поддержка защищенных сокетов с использованием библиотек OpenSSL или MbedTLS. В ближайшем будущем эта возможность будет расширяться.
Существует также экспериментальная поддержка программирования шины CAN в Linux с использованием пакета SocketCAN. Это дает адаптерам шины CAN сетевой интерфейс с ограничениями, налагаемыми протоколом сообщений CAN.
Весь код библиотеки находится в пространстве имен sockpp
C++.
Ветка «master» начинает переход к API версии 2.0 и на данный момент особенно нестабильна. Рекомендуется загрузить последнюю версию для общего использования.
Вышла версия 1.0!
Поскольку критические изменения начали накапливаться в текущей ветке разработки, было принято решение выпустить API, который был довольно стабильным в течение последних нескольких лет, как 1.0. Это из последней линейки v0.8.x. Это сделает дальнейшую работу менее запутанной и позволит нам поддерживать ветку v1.x.
Разработка версии 2.0 находится в стадии разработки.
Идея использования операций ввода-вывода без сохранения состояния, представленная в PR № 17 (которая никогда не была полностью объединена), появилась в API 2.0 с классом result<T>
. Он будет общим для возвращаемого типа «успех», а ошибки будут представлены std::error_code
. Это должно помочь значительно уменьшить проблемы платформы с отслеживанием ошибок и отчетами об ошибках.
Использование унифицированного типа результата устраняет необходимость в исключениях в большинстве функций, за исключением, возможно, конструкторов. В тех случаях, когда функция может выдать ошибку, также будет предоставлена аналогичная функция noexcept
, которая может установить параметр кода ошибки вместо выдачи. Таким образом, библиотеку можно использовать без каких-либо исключений, если того желает приложение.
Все функции, которые могут выйти из строя из-за системной ошибки, вернут результат. Это устранит необходимость в «последней ошибке», и, таким образом, исчезнет кэшированная переменная последней ошибки в классе socket
. Тогда классы сокетов будут обертывать только дескриптор сокета, что делает их более безопасными для совместного использования между потоками так же, как можно совместно использовать дескриптор - обычно с одним потоком для чтения и другим для записи.
Также начата некоторая работа по включению Secure Sockets в версию библиотеки 2.x с использованием библиотек OpenSSL или MbedTLS или (вероятно), выбора того или другого во время сборки. PR № 17, который бездействовал в течение нескольких лет, объединяется и обновляется вместе с новой работой по созданию чего-то сопоставимого с OpenSSL. При создании sockpp
вы сможете выбрать ту или иную безопасную библиотеку.
Версия 2.0 также будет переведена на C++17 и CMake v3.12 или новее.
Чтобы быть в курсе последних анонсов этого проекта, подписывайтесь на меня:
Мастодонт: @[email protected]
Твиттер: @fmpagliughi
Если вы используете эту библиотеку, напишите мне в Твиттере или отправьте мне сообщение и дайте мне знать, как вы ее используете. Мне всегда интересно посмотреть, чем это закончится!
Библиотеку после установки обычно можно обнаружить с помощью find_package(sockpp)
. Он использует пространство имен Sockpp
и имя библиотеки sockpp
.
Простой файл CMakeLists.txt может выглядеть так:
cmake_minimum_required(VERSION 3.12)
project(mysock VERSION 1.0.0)
find_package(sockpp REQUIRED)
add_executable(mysock mysock.cpp)
target_link_libraries(mysock Sockpp::sockpp)
Вклад принимается и ценится. В ветке develop
выполняется новая и нестабильная работа. Пожалуйста, отправляйте все запросы на включение в эту ветку, а не в master .
Более подробную информацию можно найти на сайте: CONTRIBUTING.md.
CMake — поддерживаемая система сборки.
Чтобы построить с параметрами по умолчанию:
$ cd sockpp
$ cmake -Bbuild .
$ cmake --build build/
Чтобы установить:
$ cmake --build build/ --target install
Библиотека имеет несколько вариантов сборки через CMake, позволяющих выбирать между созданием статической или общей (динамической) библиотеки — или того и другого. Он также позволяет создавать примеры параметров, а если установлен Doxygen, его можно использовать для создания документации.
Переменная | Значение по умолчанию | Описание |
---|---|---|
SOCKPP_BUILD_SHARED | НА | Создавать ли общую библиотеку |
SOCKPP_BUILD_STATIC | ВЫКЛЮЧЕННЫЙ | Создавать ли статическую библиотеку |
SOCKPP_BUILD_ДОКУМЕНТАЦИЯ | ВЫКЛЮЧЕННЫЙ | Создайте и установите документацию по API на основе HTML (требуется Doxygen). |
SOCKPP_BUILD_EXAMPLES | ВЫКЛЮЧЕННЫЙ | Создание примеров программ |
SOCKPP_BUILD_TESTS | ВЫКЛЮЧЕННЫЙ | Создайте модульные тесты (требуется Catch2 ). |
SOCKPP_WITH_CAN | ВЫКЛЮЧЕННЫЙ | Включите поддержку SocketCAN. (только Linux) |
Установите их с помощью переключателя «-D» в команде конфигурации CMake. Например, для создания документации и примеров приложений:
$ cd sockpp
$ cmake -Bbuild -DSOCKPP_BUILD_DOCUMENTATION=ON -DSOCKPP_BUILD_EXAMPLES=ON .
$ cmake --build build/
Чтобы создать библиотеку с поддержкой защищенных сокетов, необходимо выбрать библиотеку TLS для обеспечения поддержки. В настоящее время можно использовать OpenSSL или MbedTLS .
При настройке сборки выберите один из следующих вариантов:
Переменная | Значение по умолчанию | Описание |
---|---|---|
SOCKPP_WITH_MBEDTLS | ВЫКЛЮЧЕННЫЙ | Безопасные сокеты с MbedTLS |
SOCKPP_WITH_OPENSSL | ВЫКЛЮЧЕННЫЙ | Безопасные сокеты с OpenSSL |
Библиотека sockpp
в настоящее время поддерживает MbedTLS v3.3. При создании этой библиотеки в файле конфигурации должны быть определены следующие параметры конфигурации: include/mbedtls/mbedtls_config.h
#define MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK
Для поддержки многопоточности:
#define MBEDTLS_THREADING_PTHREAD
#define MBEDTLS_THREADING_C
и установите параметр сборки CMake:
LINK_WITH_PTHREAD:BOOL=ON
Обратите внимание, что параметры в файле конфигурации уже должны присутствовать в файле, но по умолчанию закомментированы. Просто раскомментируйте их, сохраните и создайте.
Оболочка sockpp
OpenSSL в настоящее время создается и тестируется с помощью OpenSSL v3.0.
TCP и другие «потоковые» сетевые приложения обычно настраиваются как серверы или клиенты. Приемник используется для создания сервера TCP/потоковой передачи. Он привязывает адрес и прослушивает известный порт, чтобы принять входящие соединения. Когда соединение принято, создается новый потоковый сокет. Этот новый сокет можно обработать напрямую или переместить в поток (или пул потоков) для обработки.
И наоборот, чтобы создать TCP-клиент, создается объект соединителя, который подключается к серверу по известному адресу (обычно хост и сокет). При подключении сокет является потоковым, и его можно использовать для чтения и записи напрямую.
Для IPv4 классы tcp_acceptor
и tcp_connector
используются для создания серверов и клиентов соответственно. Они используют класс inet_address
для указания адресов конечных точек, состоящих из 32-битного адреса хоста и 16-битного номера порта.
tcp_acceptor
tcp_acceptor
используется для настройки сервера и прослушивания входящих соединений.
int16_t port = 12345;
sockpp::tcp_acceptor acc(port);
if (!acc)
report_error(acc.last_error_str());
// Accept a new client connection
sockpp::tcp_socket sock = acc.accept();
Приемник обычно находится в цикле, принимая новые соединения и передавая их другому процессу, потоку или пулу потоков для взаимодействия с клиентом. В стандартном C++ это может выглядеть так:
while (true) {
// Accept a new client connection
sockpp::tcp_socket sock = acc.accept();
if (!sock) {
cerr << "Error accepting incoming connection: "
<< acc.last_error_str() << endl;
}
else {
// Create a thread and transfer the new stream to it.
thread thr(run_echo, std::move(sock));
thr.detach();
}
}
Опасности проектирования «потоков на соединение» хорошо документированы, но тот же метод можно использовать для передачи сокета в пул потоков, если он доступен.
См. пример tcpechosvr.cpp.
tcp_connector
TCP-клиент несколько проще: объект tcp_connector
создается и подключается, а затем может использоваться для непосредственного чтения и записи данных.
sockpp::tcp_connector conn;
int16_t port = 12345;
if (!conn.connect(sockpp::inet_address("localhost", port)))
report_error(conn.last_error_str());
conn.write_n("Hello", 5);
char buf[16];
ssize_t n = conn.read(buf, sizeof(buf));
См. пример tcpecho.cpp.
udp_socket
UDP-сокеты можно использовать для связи без установления соединения:
sockpp::udp_socket sock;
sockpp::inet_address addr("localhost", 12345);
std::string msg("Hello there!");
sock.send_to(msg, addr);
sockpp::inet_address srcAddr;
char buf[16];
ssize_t n = sock.recv(buf, sizeof(buf), &srcAddr);
См. примеры udpecho.cpp и udpechosvr.cpp.
Тот же стиль соединителей и приемников можно использовать для TCP-соединений через IPv6 с использованием классов:
inet6_address
tcp6_connector
tcp6_acceptor
tcp6_socket
udp6_socket
Примеры находятся в каталоге example/tcp.
То же самое справедливо и для локального подключения в системах *nix, в которых реализованы доменные сокеты Unix. Для этого используйте классы:
unix_address
unix_connector
unix_acceptor
unix_socket (unix_stream_socket)
unix_dgram_socket
Примеры находятся в каталоге example/unix.
Сеть контроллеров (CAN-шина) — это относительно простой протокол, обычно используемый микроконтроллерами для связи внутри автомобиля или промышленного оборудования. В Linux есть пакет SocketCAN , который позволяет процессам совместно использовать доступ к физическому интерфейсу CAN-шины, используя сокеты в пользовательском пространстве. См.: Linux SocketCAN.
На самом низком уровне устройства CAN записывают отдельные пакеты, называемые «фреймами», по определенным числовым адресам на шине.
Например, устройство с датчиком температуры может периодически считывать температуру и записывать ее на шину в виде необработанного 32-битного целого числа, например:
can_address addr("CAN0");
can_socket sock(addr);
// The agreed ID to broadcast temperature on the bus
canid_t canID = 0x40;
while (true) {
this_thread::sleep_for(1s);
// Write the time to the CAN bus as a 32-bit int
int32_t t = read_temperature();
can_frame frame { canID, &t, sizeof(t) };
sock.send(frame);
}
Приемник для получения кадра может выглядеть так:
can_address addr("CAN0");
can_socket sock(addr);
can_frame frame;
sock.recv(&frame);
Иерархия классов сокетов построена на основе базового класса socket
. Большинство простых приложений, вероятно, не будут использовать socket
напрямую, а будут использовать производные классы, определенные для определенного семейства адресов, например tcp_connector
и tcp_acceptor
.
Объекты сокетов хранят дескриптор дескриптора сокета базовой ОС и кэшированное значение последней ошибки, произошедшей для этого сокета. Дескриптор сокета обычно представляет собой целочисленный файловый дескриптор со значениями >=0 для открытых сокетов и -1 для неоткрытых или недействительных сокетов. Значение, используемое для неоткрытых сокетов, определяется как константа INVALID_SOCKET
, хотя обычно его не нужно проверять напрямую, поскольку сам объект будет оцениваться как false, если он не инициализирован или находится в состоянии ошибки. Типичная проверка ошибок будет такой:
tcp_connector conn({"localhost", 12345});
if (!conn)
cerr << conn.last_error_str() << std::endl;
Конструкторы по умолчанию для каждого класса сокетов ничего не делают и просто устанавливают базовый дескриптор INVALID_SOCKET
. Они не создают объект сокета. Вызов для активного подключения объекта connector
или открытия объекта- acceptor
создаст базовый сокет ОС, а затем выполнит запрошенную операцию.
Приложение обычно может выполнять большинство низкоуровневых операций с библиотекой. Неподключенные и несвязанные сокеты можно создавать с помощью статической функции create()
в большинстве классов, а затем вручную привязывать и прослушивать эти сокеты.
Метод socket::handle()
предоставляет базовый дескриптор ОС, который затем можно отправить на любой вызов API платформы, который не предоставляется библиотекой.
Объект сокета не является потокобезопасным. Приложения, которые хотят иметь несколько потоков, читающих из сокета или записывающих в сокет, должны использовать некоторую форму сериализации, например std::mutex
для защиты доступа.
socket
можно безопасно перемещать из одного потока в другой. Это обычная схема для сервера, который использует один поток для приема входящих соединений, а затем передает новый сокет другому потоку или пулу потоков для обработки. Это можно сделать следующим образом:
sockpp::tcp6_socket sock = acc.accept(&peer);
// Create a thread and transfer the new socket to it.
std::thread thr(handle_connection, std::move(sock));
В этом случае handle_connection будет функцией, которая принимает сокет по значению, например:
void handle_connection(sockpp::tcp6_socket sock) { ... }
Поскольку socket
нельзя скопировать, единственным выходом будет переместить сокет в такую функцию.
Распространенной практикой, особенно в клиентских приложениях, является наличие одного потока для чтения из сокета и другого потока для записи в сокет. В этом случае базовый дескриптор сокета можно считать потокобезопасным (один поток чтения и один поток записи). Но даже в этом сценарии объект sockpp::socket
по-прежнему не является потокобезопасным, особенно из-за кэшированного значения ошибки. Поток записи может обнаружить ошибку, произошедшую в потоке чтения, и наоборот.
Решением в этом случае является использование метода socket::clone()
для создания копии сокета. При этом будет использована системная функция dup()
или аналогичная, чтобы создать другой сокет с дублированной копией дескриптора сокета. Это дает дополнительное преимущество: каждая копия сокета может поддерживать независимое время жизни. Базовый сокет не будет закрыт до тех пор, пока оба объекта не выйдут из области видимости.
sockpp::tcp_connector conn({host, port});
auto rdSock = conn.clone();
std::thread rdThr(read_thread_func, std::move(rdSock));
Метод socket::shutdown()
можно использовать для сообщения о намерении закрыть сокет от одного из этих объектов к другому без необходимости использования другого механизма сигнализации потока.
См. пример tcpechomt.cpp.