Bibliothèque de sockets C++ simple et moderne.
Il s'agit d'un wrapper C++ de niveau assez bas autour de la bibliothèque de sockets de Berkeley utilisant des classes socket
, acceptor,
et connector
qui sont des concepts familiers dans d'autres langages.
La classe socket
de base enveloppe un handle de socket système et conserve sa durée de vie. Lorsque l’objet C++ sort de la portée, il ferme le handle de socket sous-jacent. Les objets socket sont généralement mobiles mais non copiables . Un socket peut être transféré d'une portée (ou d'un thread) à un autre en utilisant std::move()
.
La bibliothèque prend actuellement en charge : IPv4 et IPv6 sur Linux, Mac et Windows. Les autres systèmes * nix et POSIX devraient fonctionner avec peu ou pas de modifications.
Les sockets de domaine Unix sont disponibles sur les systèmes *nix qui ont une implémentation de système d'exploitation pour eux.
La prise en charge des sockets sécurisés utilisant les bibliothèques OpenSSL ou MbedTLS a été récemment ajoutée avec une couverture de base. Cela continuera à se développer dans un avenir proche.
Il existe également une prise en charge expérimentale de la programmation du bus CAN sous Linux à l'aide du package SocketCAN. Cela donne aux adaptateurs de bus CAN une interface réseau, avec des limitations dictées par le protocole de message CAN.
Tout le code de la bibliothèque réside dans l’espace de noms sockpp
C++.
La branche 'master' commence à évoluer vers l'API v2.0 et est particulièrement instable pour le moment. Il est conseillé de télécharger la dernière version pour un usage général.
La version 1.0 est sortie !
Alors que les changements radicaux commençaient à s'accumuler dans la branche de développement actuelle, la décision a été prise de publier l'API qui était assez stable ces dernières années en tant que version 1.0. Il s'agit de la dernière ligne v0.8.x. Cela rendra les choses moins confuses et nous permettra de maintenir la branche v1.x.
Le développement de la version 2.0 est en cours.
L'idée d'avoir des opérations d'E/S « sans état » introduites dans le PR #17 (qui n'a jamais été complètement fusionné) arrive dans l'API 2.0 avec une classe result<T>
. Il sera générique sur le type de retour "succès", les erreurs étant représentées par un std::error_code
. Cela devrait contribuer à réduire considérablement les problèmes de plate-forme liés au suivi et au signalement des erreurs.
L'utilisation d'un type de résultat uniforme supprime le besoin d'exceptions dans la plupart des fonctions, sauf peut-être dans les constructeurs. Dans les cas où la fonction pourrait être lancée, une fonction noexcept
comparable sera également fournie, qui peut définir un paramètre de code d'erreur au lieu de lancer. La bibliothèque peut donc être utilisée sans aucune exception si l'application le souhaite.
Toutes les fonctions susceptibles d'échouer en raison d'une erreur système renverront un résultat. Cela éliminera le besoin de la "dernière erreur", et ainsi la dernière variable d'erreur mise en cache dans la classe socket
disparaîtra. Les classes de socket encapsuleront alors uniquement le handle de socket, ce qui les rendra plus sûres à partager entre les threads de la même manière qu'un handle peut être partagé - généralement avec un thread pour la lecture et un autre pour l'écriture.
Certains travaux ont également commencé pour intégrer Secure Sockets dans une version 2.x de la bibliothèque en utilisant soit les bibliothèques OpenSSL, soit MbedTLS, ou (probablement), un choix au moment de la construction pour l'une ou l'autre. Le PR #17, qui est resté inactif depuis quelques années, est en train d'être fusionné et mis à jour, ainsi que de nouveaux travaux pour faire quelque chose de comparable avec OpenSSL. Vous pourrez choisir une bibliothèque sécurisée ou une autre lors de la construction sockpp
.
La version 2.0 passera également à C++17 et CMake v3.12 ou version ultérieure.
Pour rester au courant des dernières annonces concernant ce projet, suivez-moi sur :
Mastodonte : @[email protected]
Twitter : @fmpagliughi
Si vous utilisez cette bibliothèque, tweetez-moi ou envoyez-moi un message et dites-moi comment vous l'utilisez. Je suis toujours curieux de voir où ça aboutit !
La bibliothèque, une fois installée, peut normalement être découverte avec find_package(sockpp)
. Il utilise l'espace de noms Sockpp
et le nom de bibliothèque sockpp
.
Un simple fichier CMakeLists.txt pourrait ressembler à ceci :
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)
Les contributions sont acceptées et appréciées. Un travail nouveau et instable est effectué dans la branche develop
Veuillez soumettre toutes les demandes d'extraction sur cette branche, pas sur master .
Pour plus d'informations, reportez-vous à : CONTRIBUTING.md
CMake est le système de build pris en charge.
Pour construire avec les options par défaut :
$ cd sockpp
$ cmake -Bbuild .
$ cmake --build build/
Pour installer :
$ cmake --build build/ --target install
La bibliothèque dispose de plusieurs options de construction via CMake pour choisir entre créer une bibliothèque statique ou partagée (dynamique) - ou les deux. Il vous permet également de créer des exemples d'options, et si Doxygen est installé, il peut être utilisé pour créer de la documentation.
Variable | Valeur par défaut | Description |
---|---|---|
SOCKPP_BUILD_SHARED | SUR | S'il faut créer la bibliothèque partagée |
SOCKPP_BUILD_STATIC | DÉSACTIVÉ | S'il faut construire la bibliothèque statique |
SOCKPP_BUILD_DOCUMENTATION | DÉSACTIVÉ | Créer et installer la documentation de l'API basée sur HTML (nécessite Doxygen) |
SOCKPP_BUILD_EXAMPLES | DÉSACTIVÉ | Créer des exemples de programmes |
SOCKPP_BUILD_TESTS | DÉSACTIVÉ | Construire les tests unitaires (nécessite Catch2 ) |
SOCKPP_WITH_CAN | DÉSACTIVÉ | Inclut la prise en charge de SocketCAN. (Linux uniquement) |
Définissez-les à l'aide du commutateur « -D » dans la commande de configuration CMake. Par exemple, pour créer de la documentation et des exemples d'applications :
$ cd sockpp
$ cmake -Bbuild -DSOCKPP_BUILD_DOCUMENTATION=ON -DSOCKPP_BUILD_EXAMPLES=ON .
$ cmake --build build/
Pour créer la bibliothèque avec la prise en charge des sockets sécurisés, une bibliothèque TLS doit être choisie pour fournir la prise en charge. Actuellement, OpenSSL ou MbedTLS peuvent être utilisés.
Choisissez l'une des options suivantes lors de la configuration de la build :
Variable | Valeur par défaut | Description |
---|---|---|
SOCKPP_WITH_MBEDTLS | DÉSACTIVÉ | Sécurisez les sockets avec MbedTLS |
SOCKPP_WITH_OPENSSL | DÉSACTIVÉ | Sécurisez les sockets avec OpenSSL |
La bibliothèque sockpp
prend actuellement en charge MbedTLS v3.3. Lors de la création de cette bibliothèque, les options de configuration suivantes doivent être définies dans le fichier de configuration, include/mbedtls/mbedtls_config.h
#define MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK
Pour prendre en charge le threading :
#define MBEDTLS_THREADING_PTHREAD
#define MBEDTLS_THREADING_C
et définissez l'option de construction CMake :
LINK_WITH_PTHREAD:BOOL=ON
Notez que les options du fichier de configuration doivent déjà être présentes dans le fichier mais commentées par défaut. Décommentez-les simplement, enregistrez-les et créez-les.
Le wrapper sockpp
OpenSSL est actuellement en cours de construction et de test avec OpenSSL v3.0
TCP et autres applications réseau de « streaming » sont généralement configurées en tant que serveurs ou clients. Un accepteur est utilisé pour créer un serveur TCP/streaming. Il lie une adresse et écoute sur un port connu pour accepter les connexions entrantes. Lorsqu'une connexion est acceptée, une nouvelle socket de streaming est créée. Ce nouveau socket peut être géré directement ou déplacé vers un thread (ou un pool de threads) pour le traitement.
A l'inverse, pour créer un client TCP, un objet connecteur est créé et connecté à un serveur à une adresse connue (généralement hôte et socket). Une fois connectée, la socket est une socket de streaming qui peut être utilisée pour lire et écrire directement.
Pour IPv4, les classes tcp_acceptor
et tcp_connector
sont utilisées respectivement pour créer des serveurs et des clients. Ceux-ci utilisent la classe inet_address
pour spécifier des adresses de point de terminaison composées d'une adresse d'hôte de 32 bits et d'un numéro de port de 16 bits.
tcp_acceptor
Le tcp_acceptor
est utilisé pour configurer un serveur et écouter les connexions entrantes.
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();
L'accepteur se trouve normalement dans une boucle acceptant de nouvelles connexions et les transmet à un autre processus, thread ou pool de threads pour interagir avec le client. En C++ standard, cela pourrait ressembler à :
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();
}
}
Les dangers d'une conception thread par connexion sont bien documentés, mais la même technique peut être utilisée pour transmettre le socket dans un pool de threads, s'il en existe un.
Voir l'exemple tcpechosvr.cpp.
tcp_connector
Le client TCP est un peu plus simple dans la mesure où un objet tcp_connector
est créé et connecté, puis peut être utilisé pour lire et écrire directement des données.
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));
Voir l'exemple tcpecho.cpp.
udp_socket
Les sockets UDP peuvent être utilisées pour les communications sans connexion :
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);
Voir les exemples udpecho.cpp et udpechosvr.cpp.
Le même style de connecteurs et d'accepteurs peut être utilisé pour les connexions TCP sur IPv6 en utilisant les classes :
inet6_address
tcp6_connector
tcp6_acceptor
tcp6_socket
udp6_socket
Les exemples se trouvent dans le répertoire examples/tcp.
Il en va de même pour la connexion locale sur les systèmes *nix qui implémentent les sockets de domaine Unix. Pour cela utilisez les classes :
unix_address
unix_connector
unix_acceptor
unix_socket (unix_stream_socket)
unix_dgram_socket
Les exemples se trouvent dans le répertoire examples/unix.
Le Controller Area Network (CAN bus) est un protocole relativement simple généralement utilisé par les microcontrôleurs pour communiquer à l’intérieur d’une automobile ou d’une machine industrielle. Linux possède le package SocketCAN qui permet aux processus de partager l'accès à une interface de bus CAN physique à l'aide de sockets dans l'espace utilisateur. Voir : Linux SocketCAN
Au niveau le plus bas, les appareils CAN écrivent des paquets individuels, appelés « trames » à des adresses numériques spécifiques sur le bus.
Par exemple, un appareil doté d'un capteur de température peut lire la température de manière périodique et l'écrire sur le bus sous la forme d'un entier brut de 32 bits, comme :
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);
}
Un récepteur pour obtenir une trame pourrait ressembler à ceci :
can_address addr("CAN0");
can_socket sock(addr);
can_frame frame;
sock.recv(&frame);
La hiérarchie des classes de sockets est construite sur une classe socket
de base. La plupart des applications simples n'utiliseront probablement pas directement socket
, mais utiliseront plutôt des classes dérivées définies pour une famille d'adresses spécifique comme tcp_connector
et tcp_acceptor
.
Les objets socket conservent un handle vers un handle de socket du système d’exploitation sous-jacent et une valeur mise en cache pour la dernière erreur survenue pour ce socket. Le handle de socket est généralement un descripteur de fichier entier, avec des valeurs >=0 pour les sockets ouvertes et -1 pour une socket non ouverte ou invalide. La valeur utilisée pour les sockets non ouverts est définie comme une constante, INVALID_SOCKET
, bien qu'elle n'ait généralement pas besoin d'être testée directement, car l'objet lui-même sera évalué à false s'il n'est pas initialisé ou dans un état d'erreur. Une vérification d'erreur typique ressemblerait à ceci :
tcp_connector conn({"localhost", 12345});
if (!conn)
cerr << conn.last_error_str() << std::endl;
Les constructeurs par défaut de chacune des classes de socket ne font rien et définissent simplement le handle sous-jacent sur INVALID_SOCKET
. Ils ne créent pas d'objet socket. L'appel pour connecter activement un objet connector
ou ouvrir un objet acceptor
créera un socket de système d'exploitation sous-jacent, puis effectuera l'opération demandée.
Une application peut généralement effectuer la plupart des opérations de bas niveau avec la bibliothèque. Des sockets non connectés et non liés peuvent être créés avec la fonction statique create()
dans la plupart des classes, puis liés et écoutés manuellement sur ces sockets.
La méthode socket::handle()
expose le handle du système d'exploitation sous-jacent qui peut ensuite être envoyé à n'importe quel appel d'API de plate-forme qui n'est pas exposé par la bibliothèque.
Un objet socket n’est pas thread-safe. Les applications qui souhaitent que plusieurs threads lisent à partir d'un socket ou écrivent sur un socket doivent utiliser une forme de sérialisation, telle qu'un std::mutex
pour protéger l'accès.
Un socket
peut être déplacé d’un thread à un autre en toute sécurité. Il s'agit d'un modèle courant pour un serveur qui utilise un thread pour accepter les connexions entrantes, puis transmet le nouveau socket à un autre thread ou pool de threads pour la gestion. Cela peut être fait comme :
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));
Dans ce cas, handle_connection serait une fonction qui prend une socket par valeur, comme :
void handle_connection(sockpp::tcp6_socket sock) { ... }
Puisqu'un socket
ne peut pas être copié, le seul choix serait de déplacer le socket vers une fonction comme celle-ci.
Il est courant, en particulier dans les applications clientes, d'avoir un thread pour lire à partir d'un socket et un autre thread pour écrire sur le socket. Dans ce cas, le handle de socket sous-jacent peut être considéré comme thread-safe (un thread de lecture et un thread d’écriture). Mais même dans ce scénario, un objet sockpp::socket
n'est toujours pas thread-safe en raison notamment de la valeur d'erreur mise en cache. Le fil d'écriture peut voir une erreur survenue sur le fil de lecture et vice versa.
La solution dans ce cas est d’utiliser la méthode socket::clone()
pour faire une copie du socket. Cela utilisera la fonction dup()
du système ou similaire pour créer un autre socket avec une copie dupliquée du handle de socket. Cela présente l'avantage supplémentaire que chaque copie du socket peut conserver une durée de vie indépendante. Le socket sous-jacent ne sera pas fermé tant que les deux objets ne seront pas hors de portée.
sockpp::tcp_connector conn({host, port});
auto rdSock = conn.clone();
std::thread rdThr(read_thread_func, std::move(rdSock));
La méthode socket::shutdown()
peut être utilisée pour communiquer l'intention de fermer le socket de l'un de ces objets à l'autre sans avoir besoin d'un autre mécanisme de signalisation de thread.
Voir l'exemple tcpechomt.cpp.