Einfache, moderne C++-Socket-Bibliothek.
Dabei handelt es sich um einen ziemlich einfachen C++-Wrapper um die Berkeley-Sockets-Bibliothek, der socket
, acceptor,
und connector
Klassen verwendet, bei denen es sich um bekannte Konzepte aus anderen Sprachen handelt.
Die Basis socket
Klasse umschließt ein System-Socket-Handle und behält dessen Lebensdauer bei. Wenn das C++-Objekt den Gültigkeitsbereich verlässt, wird das zugrunde liegende Socket-Handle geschlossen. Socket-Objekte sind im Allgemeinen verschiebbar , aber nicht kopierbar . Ein Socket kann mit std::move()
von einem Bereich (oder Thread) in einen anderen übertragen werden.
Die Bibliothek unterstützt derzeit: IPv4 und IPv6 unter Linux, Mac und Windows. Andere *nix- und POSIX-Systeme sollten mit geringen oder keinen Änderungen funktionieren.
Unix-Domain-Sockets sind auf *nix-Systemen verfügbar, die über eine Betriebssystemimplementierung dafür verfügen.
Unterstützung für sichere Sockets, die entweder die OpenSSL- oder MbedTLS-Bibliotheken verwenden, wurde kürzlich mit grundlegender Abdeckung hinzugefügt. Dies wird in naher Zukunft weiter ausgebaut.
Es gibt auch experimentelle Unterstützung für die CAN-Bus-Programmierung unter Linux mithilfe des SocketCAN-Pakets. Dadurch erhalten CAN-Bus-Adapter eine Netzwerkschnittstelle, wobei Einschränkungen durch das CAN-Nachrichtenprotokoll vorgegeben sind.
Der gesamte Code in der Bibliothek befindet sich im C++-Namespace sockpp
.
Der „Master“-Zweig beginnt mit der Umstellung auf die v2.0-API und ist derzeit besonders instabil. Wir empfehlen Ihnen, die neueste Version für den allgemeinen Gebrauch herunterzuladen.
Version 1.0 ist veröffentlicht!
Da sich im aktuellen Entwicklungszweig immer mehr bahnbrechende Änderungen häuften, wurde die Entscheidung getroffen, die API, die in den letzten Jahren recht stabil war, als 1.0 zu veröffentlichen. Dies ist aus der neuesten Version 0.8.x. Dadurch wird es in Zukunft weniger verwirrend und wir können den v1.x-Zweig beibehalten.
Die Entwicklung der Version 2.0 ist im Gange.
Die Idee, in PR Nr. 17 „zustandslose“ E/A-Operationen einzuführen (die nie vollständig zusammengeführt wurden), kommt in der 2.0-API mit einer result<T>
-Klasse zum Einsatz. Es ist generisch gegenüber dem Rückgabetyp „success“, wobei Fehler durch einen std::error_code
dargestellt werden. Dies sollte dazu beitragen, Plattformprobleme bei der Verfolgung und Meldung von Fehlern deutlich zu reduzieren.
Die Verwendung eines einheitlichen Ergebnistyps macht Ausnahmen in den meisten Funktionen überflüssig, außer vielleicht in Konstruktoren. In den Fällen, in denen die Funktion möglicherweise einen Fehler auslöst, wird auch eine vergleichbare Funktion „ noexcept
bereitgestellt, die einen Fehlercodeparameter festlegen kann, anstatt einen Fehler auszulösen. Somit kann die Bibliothek ausnahmslos genutzt werden, wenn die Anwendung dies wünscht.
Alle Funktionen, die aufgrund eines Systemfehlers fehlschlagen könnten, geben ein Ergebnis zurück. Dadurch entfällt die Notwendigkeit des „letzten Fehlers“ und somit verschwindet die zwischengespeicherte Variable „letzter Fehler“ in der socket
-Klasse. Die Socket-Klassen umschließen dann nur das Socket-Handle, wodurch die gemeinsame Nutzung zwischen Threads auf die gleiche Weise sicherer wird, wie ein Handle gemeinsam genutzt werden kann – typischerweise mit einem Thread zum Lesen und einem anderen zum Schreiben.
Einige Arbeiten haben auch damit begonnen, Secure Sockets in eine 2.x-Version der Bibliothek zu integrieren, indem entweder OpenSSL- oder MbedTLS-Bibliotheken oder (wahrscheinlich) eine Build-Zeit-Wahl für die eine oder andere verwendet werden. PR #17, das seit einigen Jahren ruht, wird zusammengeführt und aktualisiert, zusammen mit neuen Arbeiten, um etwas Vergleichbares mit OpenSSL zu schaffen. Sie können beim Erstellen von sockpp
die eine oder andere sichere Bibliothek auswählen.
Die Version 2.0 wird auch auf C++17 und CMake v3.12 oder höher umsteigen.
Um über die neuesten Ankündigungen zu diesem Projekt auf dem Laufenden zu bleiben, folgen Sie mir unter:
Mastodon: @[email protected]
Twitter: @fmpagliughi
Wenn Sie diese Bibliothek verwenden, twittern Sie mir oder senden Sie mir eine Nachricht und teilen Sie mir mit, wie Sie sie verwenden. Ich bin immer gespannt, wo es landet!
Wenn die Bibliothek installiert ist, kann sie normalerweise mit find_package(sockpp)
ermittelt werden. Es verwendet den Namensraum Sockpp
und den Bibliotheksnamen sockpp
.
Eine einfache CMakeLists.txt- Datei könnte so aussehen:
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)
Beiträge werden angenommen und geschätzt. Im develop
werden neue und instabile Arbeiten ausgeführt. Bitte senden Sie alle Pull-Anfragen an diesen Zweig, nicht an den Master .
Weitere Informationen finden Sie unter: CONTRIBUTING.md
CMake ist das unterstützte Build-System.
So erstellen Sie mit Standardoptionen:
$ cd sockpp
$ cmake -Bbuild .
$ cmake --build build/
Zur Installation:
$ cmake --build build/ --target install
Die Bibliothek verfügt über mehrere Build-Optionen über CMake, mit denen Sie zwischen der Erstellung einer statischen oder einer gemeinsam genutzten (dynamischen) Bibliothek – oder beiden – wählen können. Es ermöglicht Ihnen auch, die Beispieloptionen zu erstellen, und wenn Doxygen installiert ist, kann es zum Erstellen von Dokumentationen verwendet werden.
Variable | Standardwert | Beschreibung |
---|---|---|
SOCKPP_BUILD_SHARED | AN | Ob die gemeinsam genutzte Bibliothek erstellt werden soll |
SOCKPP_BUILD_STATIC | AUS | Ob die statische Bibliothek erstellt werden soll |
SOCKPP_BUILD_DOCUMENTATION | AUS | Erstellen und installieren Sie die HTML-basierte API-Dokumentation (erfordert Doxygen). |
SOCKPP_BUILD_EXAMPLES | AUS | Erstellen Sie Beispielprogramme |
SOCKPP_BUILD_TESTS | AUS | Erstellen Sie die Unit-Tests (erfordert Catch2 ) |
SOCKPP_WITH_CAN | AUS | Einschließlich SocketCAN-Unterstützung. (Nur Linux) |
Legen Sie diese mit dem Schalter „-D“ im CMake-Konfigurationsbefehl fest. So erstellen Sie beispielsweise Dokumentation und Beispiel-Apps:
$ cd sockpp
$ cmake -Bbuild -DSOCKPP_BUILD_DOCUMENTATION=ON -DSOCKPP_BUILD_EXAMPLES=ON .
$ cmake --build build/
Um die Bibliothek mit Secure-Socket-Unterstützung zu erstellen, muss eine TLS-Bibliothek ausgewählt werden, die Unterstützung bietet. Derzeit können OpenSSL oder MbedTLS verwendet werden.
Wählen Sie beim Konfigurieren des Builds eine der folgenden Optionen:
Variable | Standardwert | Beschreibung |
---|---|---|
SOCKPP_WITH_MBEDTLS | AUS | Sichere Sockets mit MbedTLS |
SOCKPP_WITH_OPENSSL | AUS | Sichere Sockets mit OpenSSL |
Die sockpp
-Bibliothek unterstützt derzeit MbedTLS v3.3. Beim Erstellen dieser Bibliothek sollten die folgenden Konfigurationsoptionen in der Konfigurationsdatei include/mbedtls/mbedtls_config.h definiert werden
#define MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK
Um das Einfädeln zu unterstützen:
#define MBEDTLS_THREADING_PTHREAD
#define MBEDTLS_THREADING_C
und legen Sie die CMake-Build-Option fest:
LINK_WITH_PTHREAD:BOOL=ON
Beachten Sie, dass die Optionen in der Konfigurationsdatei bereits in der Datei vorhanden, aber standardmäßig auskommentiert sein sollten. Kommentieren Sie sie einfach aus, speichern Sie sie und erstellen Sie sie.
Der sockpp
OpenSSL-Wrapper wird derzeit mit OpenSSL v3.0 erstellt und getestet
TCP und andere „Streaming“-Netzwerkanwendungen werden normalerweise entweder als Server oder als Clients eingerichtet. Ein Akzeptor wird zum Erstellen eines TCP/Streaming-Servers verwendet. Es bindet eine Adresse und lauscht an einem bekannten Port, um eingehende Verbindungen zu akzeptieren. Wenn eine Verbindung akzeptiert wird, wird ein neuer Streaming-Socket erstellt. Dieser neue Socket kann direkt verarbeitet oder zur Verarbeitung in einen Thread (oder Thread-Pool) verschoben werden.
Umgekehrt wird zum Erstellen eines TCP-Clients ein Connector-Objekt erstellt und an einer bekannten Adresse (normalerweise Host und Socket) mit einem Server verbunden. Wenn der Socket angeschlossen ist, handelt es sich um einen Streaming-Socket, der direkt zum Lesen und Schreiben verwendet werden kann.
Für IPv4 werden die Klassen tcp_acceptor
und tcp_connector
zum Erstellen von Servern bzw. Clients verwendet. Diese verwenden die Klasse inet_address
, um Endpunktadressen anzugeben, die aus einer 32-Bit-Hostadresse und einer 16-Bit-Portnummer bestehen.
tcp_acceptor
Der tcp_acceptor
wird verwendet, um einen Server einzurichten und auf eingehende Verbindungen zu warten.
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();
Der Akzeptor befindet sich normalerweise in einer Schleife, in der er neue Verbindungen akzeptiert und diese an einen anderen Prozess, Thread oder Thread-Pool weiterleitet, um mit dem Client zu interagieren. In Standard-C++ könnte dies so aussehen:
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();
}
}
Die Gefahren eines Thread-pro-Verbindung-Designs sind gut dokumentiert, aber die gleiche Technik kann verwendet werden, um den Socket an einen Thread-Pool weiterzuleiten, sofern einer verfügbar ist.
Sehen Sie sich das Beispiel tcpechosvr.cpp an.
tcp_connector
Der TCP-Client ist etwas einfacher, da ein tcp_connector
Objekt erstellt und verbunden wird, das dann zum direkten Lesen und Schreiben von Daten verwendet werden kann.
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));
Siehe das Beispiel tcpecho.cpp.
udp_socket
UDP-Sockets können für verbindungslose Kommunikation verwendet werden:
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);
Sehen Sie sich die Beispiele udpecho.cpp und udpechosvr.cpp an.
Für TCP-Verbindungen über IPv6 können mit den folgenden Klassen dieselben Konnektoren und Akzeptoren verwendet werden:
inet6_address
tcp6_connector
tcp6_acceptor
tcp6_socket
udp6_socket
Beispiele finden Sie im Verzeichnis examples/tcp.
Das Gleiche gilt für lokale Verbindungen auf *nix-Systemen, die Unix-Domänen-Sockets implementieren. Verwenden Sie dazu die Klassen:
unix_address
unix_connector
unix_acceptor
unix_socket (unix_stream_socket)
unix_dgram_socket
Beispiele finden Sie im Verzeichnis examples/unix.
Das Controller Area Network (CAN-Bus) ist ein relativ einfaches Protokoll, das typischerweise von Mikrocontrollern zur Kommunikation innerhalb eines Automobils oder einer Industriemaschine verwendet wird. Linux verfügt über das SocketCAN -Paket, das es Prozessen ermöglicht, über Sockets im Benutzerbereich gemeinsam auf eine physische CAN-Bus-Schnittstelle zuzugreifen. Siehe: Linux SocketCAN
Auf der untersten Ebene schreiben CAN-Geräte einzelne Pakete, sogenannte „Frames“, an bestimmte numerische Adressen auf dem Bus.
Beispielsweise könnte ein Gerät mit einem Temperatursensor die Temperatur periodisch lesen und als rohe 32-Bit-Ganzzahl auf den Bus schreiben, wie zum Beispiel:
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);
}
Ein Empfänger, der einen Frame erhält, könnte folgendermaßen aussehen:
can_address addr("CAN0");
can_socket sock(addr);
can_frame frame;
sock.recv(&frame);
Die Socket-Klassenhierarchie basiert auf einer Basis socket
-Klasse. Die meisten einfachen Anwendungen verwenden socket
wahrscheinlich nicht direkt, sondern abgeleitete Klassen, die für eine bestimmte Adressfamilie definiert sind, wie tcp_connector
und tcp_acceptor
.
Die Socket-Objekte behalten ein Handle für ein zugrunde liegendes Betriebssystem-Socket-Handle und einen zwischengespeicherten Wert für den letzten Fehler, der für diesen Socket aufgetreten ist. Das Socket-Handle ist normalerweise ein ganzzahliger Dateideskriptor mit Werten >=0 für offene Sockets und -1 für einen ungeöffneten oder ungültigen Socket. Der für ungeöffnete Sockets verwendete Wert ist als Konstante INVALID_SOCKET
definiert, obwohl er normalerweise nicht direkt getestet werden muss, da das Objekt selbst als falsch ausgewertet wird, wenn es nicht initialisiert ist oder sich in einem Fehlerzustand befindet. Eine typische Fehlerprüfung würde wie folgt aussehen:
tcp_connector conn({"localhost", 12345});
if (!conn)
cerr << conn.last_error_str() << std::endl;
Die Standardkonstruktoren für jede der Socket-Klassen tun nichts und setzen einfach das zugrunde liegende Handle auf INVALID_SOCKET
. Sie erstellen kein Socket-Objekt. Der Aufruf zum aktiven Verbinden eines connector
-Objekts oder zum Öffnen eines acceptor
-Objekts erstellt einen zugrunde liegenden OS-Socket und führt dann den angeforderten Vorgang aus.
Eine Anwendung kann im Allgemeinen die meisten Vorgänge auf niedriger Ebene mit der Bibliothek ausführen. Nicht verbundene und ungebundene Sockets können in den meisten Klassen mit der statischen Funktion create()
erstellt werden und diese Sockets dann manuell binden und abhören.
Die Methode socket::handle()
stellt das zugrunde liegende Betriebssystem-Handle bereit, das dann an jeden Plattform-API-Aufruf gesendet werden kann, der nicht von der Bibliothek verfügbar gemacht wird.
Ein Socket-Objekt ist nicht threadsicher. Anwendungen, bei denen mehrere Threads von einem Socket lesen oder in einen Socket schreiben sollen, sollten eine Form der Serialisierung verwenden, z. B. einen std::mutex
um den Zugriff zu schützen.
Ein socket
kann sicher von einem Thread in einen anderen verschoben werden. Dies ist ein gängiges Muster für einen Server, der einen Thread zum Annehmen eingehender Verbindungen verwendet und den neuen Socket dann zur Verarbeitung an einen anderen Thread oder Thread-Pool weitergibt. Dies kann wie folgt erfolgen:
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));
In diesem Fall wäre handle_connection eine Funktion, die einen Socket als Wert annimmt, wie zum Beispiel:
void handle_connection(sockpp::tcp6_socket sock) { ... }
Da ein socket
nicht kopiert werden kann, besteht die einzige Möglichkeit darin, den Socket in eine Funktion wie diese zu verschieben.
Es ist ein gängiges Muster, insbesondere in Clientanwendungen, dass ein Thread aus einem Socket liest und ein anderer Thread in den Socket schreibt. In diesem Fall kann das zugrunde liegende Socket-Handle als Thread-sicher betrachtet werden (ein Lese-Thread und ein Schreib-Thread). Aber selbst in diesem Szenario ist ein sockpp::socket
Objekt insbesondere aufgrund des zwischengespeicherten Fehlerwerts immer noch nicht threadsicher. Der Schreibthread erkennt möglicherweise einen Fehler, der im Lesethread aufgetreten ist, und umgekehrt.
Die Lösung für diesen Fall besteht darin, mit der Methode socket::clone()
eine Kopie des Sockets zu erstellen. Dabei wird die Systemfunktion dup()
oder ähnliches verwendet, um einen weiteren Socket mit einer duplizierten Kopie des Socket-Handles zu erstellen. Dies hat den zusätzlichen Vorteil, dass jede Kopie des Sockets eine unabhängige Lebensdauer behalten kann. Der zugrunde liegende Socket wird erst geschlossen, wenn beide Objekte den Gültigkeitsbereich verlassen.
sockpp::tcp_connector conn({host, port});
auto rdSock = conn.clone();
std::thread rdThr(read_thread_func, std::move(rdSock));
Die Methode socket::shutdown()
kann verwendet werden, um die Absicht, den Socket von einem dieser Objekte zu schließen, dem anderen mitzuteilen, ohne dass ein weiterer Thread-Signalisierungsmechanismus erforderlich ist.
Siehe das Beispiel tcpechomt.cpp.