Biblioteca de sockets C++ sencilla y moderna.
Este es un contenedor de C++ de nivel bastante bajo para la biblioteca de sockets de Berkeley que utiliza clases socket
, acceptor,
y connector
que son conceptos familiares de otros lenguajes.
La clase socket
base envuelve un identificador de socket del sistema y mantiene su vida útil. Cuando el objeto C++ sale del alcance, cierra el identificador del socket subyacente. Los objetos de socket generalmente son móviles pero no copiables . Un socket se puede transferir de un ámbito (o hilo) a otro usando std::move()
.
Actualmente, la biblioteca admite: IPv4 e IPv6 en Linux, Mac y Windows. Otros sistemas *nix y POSIX deberían funcionar con poca o ninguna modificación.
Los sockets de dominio Unix están disponibles en sistemas *nix que tienen una implementación de sistema operativo para ellos.
Recientemente se agregó con cobertura básica soporte para sockets seguros usando las bibliotecas OpenSSL o MbedTLS. Esto seguirá ampliándose en un futuro próximo.
También existe cierto soporte experimental para la programación del bus CAN en Linux utilizando el paquete SocketCAN. Esto proporciona a los adaptadores de bus CAN una interfaz de red, con limitaciones dictadas por el protocolo de mensajes CAN.
Todo el código de la biblioteca se encuentra dentro del espacio de nombres sockpp
C++.
La rama 'master' está comenzando a avanzar hacia la API v2.0 y es particularmente inestable en este momento. Se recomienda descargar la última versión para uso general.
¡Se lanza la versión 1.0!
A medida que se empezaban a acumular cambios importantes en la rama de desarrollo actual, se tomó la decisión de lanzar la API que ha sido bastante estable durante los últimos años como 1.0. Esto es de la última línea v0.8.x. Eso hará que las cosas en el futuro sean menos confusas y nos permitirá mantener la rama v1.x.
El desarrollo de la versión 2.0 está en marcha.
La idea de tener operaciones de E/S "sin estado" introducidas en el PR #17 (que nunca se fusionó por completo) viene en la API 2.0 con una clase result<T>
. Será genérico sobre el tipo de devolución "éxito" y los errores se representarán mediante std::error_code
. Esto debería ayudar a reducir significativamente los problemas de la plataforma relacionados con el seguimiento y la notificación de errores.
El uso de un tipo de resultado uniforme elimina la necesidad de excepciones en la mayoría de las funciones, excepto quizás en los constructores. En aquellos casos en los que la función pueda generar un error, también se proporcionará una función noexcept
comparable que puede establecer un parámetro de código de error en lugar de generar un error. Por lo tanto, la biblioteca se puede utilizar sin excepciones si así lo desea la aplicación.
Todas las funciones que puedan fallar debido a un error del sistema devolverán un resultado. Eso eliminará la necesidad del "último error" y, por lo tanto, la variable del último error almacenada en caché en la clase socket
desaparecerá. Las clases de socket entonces solo envolverán el identificador del socket, lo que las hará más seguras para compartir entre subprocesos de la misma manera que se puede compartir un identificador, generalmente con un subproceso para lectura y otro para escritura.
También se ha comenzado a trabajar para incorporar Secure Sockets en una versión 2.x de la biblioteca utilizando bibliotecas OpenSSL o MbedTLS o (probablemente), una opción de tiempo de compilación para una u otra. El PR #17, que ha estado inactivo durante algunos años, se está fusionando y actualizando, junto con un nuevo trabajo para hacer algo comparable con OpenSSL. Podrás elegir una biblioteca segura u otra al crear sockpp
.
La versión 2.0 también pasará a C++ 17 y CMake v3.12 o posterior.
Para mantenerse al día con los últimos anuncios de este proyecto, sígueme en:
Mastodonte: @[email protected]
Gorjeo: @fmpagliughi
Si estás usando esta biblioteca, envíame un tweet o envíame un mensaje y cuéntame cómo la estás usando. ¡Siempre tengo curiosidad por ver dónde termina!
La biblioteca, cuando está instalada, normalmente se puede descubrir con find_package(sockpp)
. Utiliza el espacio de nombres Sockpp
y el nombre de la biblioteca sockpp
.
Un archivo CMakeLists.txt simple podría verse así:
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)
Se aceptan y agradecen las contribuciones. Se realiza trabajo nuevo e inestable en la rama develop
. Envíe todas las solicitudes de extracción en esa rama, no en master .
Para obtener más información, consulte: CONTRIBUTING.md
CMake es el sistema de compilación compatible.
Para construir con opciones predeterminadas:
$ cd sockpp
$ cmake -Bbuild .
$ cmake --build build/
Para instalar:
$ cmake --build build/ --target install
La biblioteca tiene varias opciones de compilación a través de CMake para elegir entre crear una biblioteca estática o compartida (dinámica), o ambas. También le permite crear opciones de ejemplo y, si Doxygen está instalado, puede usarse para crear documentación.
Variable | Valor predeterminado | Descripción |
---|---|---|
SOCKPP_BUILD_SHARED | EN | Ya sea para construir la biblioteca compartida |
SOCKPP_BUILD_STATIC | APAGADO | Ya sea para construir la biblioteca estática |
SOCKPP_BUILD_DOCUMENTATION | APAGADO | Cree e instale la documentación API basada en HTML (requiere Doxygen) |
SOCKPP_BUILD_EXAMPLES | APAGADO | Construir programas de ejemplo |
SOCKPP_BUILD_TESTS | APAGADO | Construya las pruebas unitarias (requiere Catch2 ) |
SOCKPP_WITH_CAN | APAGADO | Incluye soporte SocketCAN. (Solo Linux) |
Configúrelos usando el modificador '-D' en el comando de configuración de CMake. Por ejemplo, para crear documentación y aplicaciones de ejemplo:
$ cd sockpp
$ cmake -Bbuild -DSOCKPP_BUILD_DOCUMENTATION=ON -DSOCKPP_BUILD_EXAMPLES=ON .
$ cmake --build build/
Para crear la biblioteca con soporte de socket seguro, se debe elegir una biblioteca TLS para brindar soporte. Actualmente se puede utilizar OpenSSL o MbedTLS .
Elija una de las siguientes opciones al configurar la compilación:
Variable | Valor predeterminado | Descripción |
---|---|---|
SOCKPP_WITH_MBEDTLS | APAGADO | Enchufes seguros con MbedTLS |
SOCKPP_WITH_OPENSSL | APAGADO | Sockets seguros con OpenSSL |
La biblioteca sockpp
actualmente admite MbedTLS v3.3. Al crear esa biblioteca, se deben definir las siguientes opciones de configuración en el archivo de configuración, include/mbedtls/mbedtls_config.h
#define MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK
Para admitir subprocesos:
#define MBEDTLS_THREADING_PTHREAD
#define MBEDTLS_THREADING_C
y configure la opción de compilación CMake:
LINK_WITH_PTHREAD:BOOL=ON
Tenga en cuenta que las opciones del archivo de configuración ya deberían estar presentes en el archivo, pero comentadas de forma predeterminada. Simplemente descomentelos, guárdelos y construya.
El contenedor sockpp
OpenSSL se está construyendo y probando actualmente con OpenSSL v3.0.
TCP y otras aplicaciones de red de "transmisión" generalmente se configuran como servidores o clientes. Se utiliza un aceptador para crear un servidor TCP/streaming. Vincula una dirección y escucha en un puerto conocido para aceptar conexiones entrantes. Cuando se acepta una conexión, se crea un nuevo socket de transmisión. Ese nuevo socket se puede manejar directamente o mover a un subproceso (o grupo de subprocesos) para su procesamiento.
Por el contrario, para crear un cliente TCP, se crea un objeto conector y se conecta a un servidor en una dirección conocida (normalmente host y socket). Cuando está conectado, el socket es de transmisión que se puede usar para leer y escribir directamente.
Para IPv4, las clases tcp_acceptor
y tcp_connector
se utilizan para crear servidores y clientes, respectivamente. Estos utilizan la clase inet_address
para especificar direcciones de punto final compuestas por una dirección de host de 32 bits y un número de puerto de 16 bits.
tcp_acceptor
tcp_acceptor
se utiliza para configurar un servidor y escuchar conexiones 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();
El aceptador normalmente se encuentra en un bucle que acepta nuevas conexiones y las pasa a otro proceso, subproceso o grupo de subprocesos para interactuar con el cliente. En C++ estándar, esto podría verse así:
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();
}
}
Los peligros de un diseño de subproceso por conexión están bien documentados, pero se puede utilizar la misma técnica para pasar el socket a un grupo de subprocesos, si hay uno disponible.
Vea el ejemplo de tcpechosvr.cpp.
tcp_connector
El cliente TCP es algo más simple en el sentido de que se crea y conecta un objeto tcp_connector
, que luego se puede usar para leer y escribir datos directamente.
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));
Vea el ejemplo de tcpecho.cpp.
udp_socket
Los sockets UDP se pueden utilizar para comunicaciones sin conexión:
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);
Vea los ejemplos de udpecho.cpp y udpechosvr.cpp.
Se puede utilizar el mismo estilo de conectores y aceptadores para conexiones TCP sobre IPv6 utilizando las clases:
inet6_address
tcp6_connector
tcp6_acceptor
tcp6_socket
udp6_socket
Los ejemplos se encuentran en el directorio ejemplos/tcp.
Lo mismo ocurre con la conexión local en sistemas *nix que implementan Unix Domain Sockets. Para eso usa las clases:
unix_address
unix_connector
unix_acceptor
unix_socket (unix_stream_socket)
unix_dgram_socket
Los ejemplos se encuentran en el directorio ejemplos/unix.
La red de área del controlador (bus CAN) es un protocolo relativamente simple que suelen utilizar los microcontroladores para comunicarse dentro de un automóvil o una máquina industrial. Linux tiene el paquete SocketCAN que permite a los procesos compartir el acceso a una interfaz de bus CAN física utilizando sockets en el espacio del usuario. Ver: Linux SocketCAN
En el nivel más bajo, los dispositivos CAN escriben paquetes individuales, llamados "tramas", en direcciones numéricas específicas en el bus.
Por ejemplo, un dispositivo con un sensor de temperatura podría leer la temperatura periódicamente y escribirla en el bus como un entero sin formato de 32 bits, como:
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 receptor para obtener una trama podría verse así:
can_address addr("CAN0");
can_socket sock(addr);
can_frame frame;
sock.recv(&frame);
La jerarquía de clases de socket se basa en una clase socket
base. La mayoría de las aplicaciones simples probablemente no usarán socket
directamente, sino que usarán clases derivadas definidas para una familia de direcciones específica como tcp_connector
y tcp_acceptor
.
Los objetos de socket mantienen un identificador de un identificador de socket del sistema operativo subyacente y un valor en caché para el último error que ocurrió para ese socket. El identificador del socket suele ser un descriptor de archivo entero, con valores >=0 para sockets abiertos y -1 para un socket no abierto o no válido. El valor utilizado para los sockets sin abrir se define como una constante, INVALID_SOCKET
, aunque generalmente no es necesario probarlo directamente, ya que el objeto en sí se evaluará como falso si no está inicializado o se encuentra en un estado de error. Una comprobación de error típica sería así:
tcp_connector conn({"localhost", 12345});
if (!conn)
cerr << conn.last_error_str() << std::endl;
Los constructores predeterminados para cada una de las clases de socket no hacen nada y simplemente establecen el identificador subyacente en INVALID_SOCKET
. No crean un objeto de socket. La llamada para conectar activamente un objeto connector
o abrir un objeto acceptor
creará un socket del sistema operativo subyacente y luego realizará la operación solicitada.
Generalmente, una aplicación puede realizar la mayoría de las operaciones de bajo nivel con la biblioteca. Se pueden crear sockets desconectados y no vinculados con la función estática create()
en la mayoría de las clases, y luego vincularlos y escucharlos manualmente.
El método socket::handle()
expone el identificador del sistema operativo subyacente que luego se puede enviar a cualquier llamada API de plataforma que no esté expuesta por la biblioteca.
Un objeto socket no es seguro para subprocesos. Las aplicaciones que desean tener múltiples subprocesos leyendo desde un socket o escribiendo en un socket deben usar alguna forma de serialización, como std::mutex
para proteger el acceso.
Un socket
se puede mover de un hilo a otro de forma segura. Este es un patrón común para un servidor que utiliza un subproceso para aceptar conexiones entrantes y luego pasa el nuevo socket a otro subproceso o grupo de subprocesos para su manejo. Esto se puede hacer como:
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));
En este caso, handle_connection sería una función que toma un socket por valor, como:
void handle_connection(sockpp::tcp6_socket sock) { ... }
Dado que un socket
no se puede copiar, la única opción sería mover el socket a una función como esta.
Es un patrón común, especialmente en aplicaciones cliente, tener un hilo para leer desde un socket y otro hilo para escribir en el socket. En este caso, el identificador del socket subyacente puede considerarse seguro para subprocesos (un subproceso de lectura y otro de escritura). Pero incluso en este escenario, un objeto sockpp::socket
todavía no es seguro para subprocesos debido especialmente al valor de error almacenado en caché. El hilo de escritura podría ver un error que ocurrió en el hilo de lectura y viceversa.
La solución para este caso es utilizar el método socket::clone()
para hacer una copia del socket. Esto utilizará la función dup()
del sistema o similar para crear otro socket con una copia duplicada del identificador del socket. Esto tiene el beneficio adicional de que cada copia del socket puede mantener una vida útil independiente. El socket subyacente no se cerrará hasta que ambos objetos salgan del alcance.
sockpp::tcp_connector conn({host, port});
auto rdSock = conn.clone();
std::thread rdThr(read_thread_func, std::move(rdSock));
El método socket::shutdown()
se puede utilizar para comunicar la intención de cerrar el socket de uno de estos objetos al otro sin necesidad de otro mecanismo de señalización de subprocesos.
Vea el ejemplo de tcpechomt.cpp.