Biblioteca de soquete C++ simples e moderna.
Este é um wrapper C++ de nível bastante baixo em torno da biblioteca de soquetes Berkeley usando classes socket
, acceptor,
e connector
que são conceitos familiares de outras linguagens.
A classe socket
base envolve um identificador de soquete do sistema e mantém seu tempo de vida. Quando o objeto C++ sai do escopo, ele fecha o identificador de soquete subjacente. Objetos de soquete geralmente são móveis , mas não copiáveis . Um soquete pode ser transferido de um escopo (ou thread) para outro usando std::move()
.
A biblioteca atualmente suporta: IPv4 e IPv6 em Linux, Mac e Windows. Outros sistemas *nix e POSIX devem funcionar com pouca ou nenhuma modificação.
Os soquetes de domínio Unix estão disponíveis em sistemas *nix que possuem uma implementação de sistema operacional para eles.
O suporte para soquetes seguros usando as bibliotecas OpenSSL ou MbedTLS foi adicionado recentemente com cobertura básica. Isso continuará a ser expandido no futuro próximo.
Há também algum suporte experimental para programação de barramento CAN no Linux usando o pacote SocketCAN. Isto dá aos adaptadores de barramento CAN uma interface de rede, com limitações ditadas pelo protocolo de mensagens CAN.
Todo o código da biblioteca reside no namespace sockpp
C++.
O branch 'master' está iniciando a mudança em direção à API v2.0 e está particularmente instável no momento. É aconselhável baixar a versão mais recente para uso geral.
A versão 1.0 foi lançada!
Como as mudanças mais recentes começaram a se acumular no ramo de desenvolvimento atual, foi tomada a decisão de lançar a API que tem sido bastante estável nos últimos anos como 1.0. Isto é da linha v0.8.x mais recente. Isso tornará as coisas menos confusas e nos permitirá manter o branch v1.x.
O desenvolvimento da versão 2.0 está em andamento.
A ideia de ter operações de E/S "sem estado" introduzidas no PR #17 (que nunca foi totalmente mesclada) está chegando na API 2.0 com uma classe result<T>
. Será genérico em relação ao tipo de retorno "sucesso", com erros sendo representados por um std::error_code
. Isso deve ajudar a reduzir significativamente os problemas da plataforma para rastrear e relatar erros.
Usar um tipo de resultado uniforme elimina a necessidade de exceções na maioria das funções, exceto talvez nos construtores. Nos casos em que a função possa ser lançada, também será fornecida uma função noexcept
comparável, que pode definir um parâmetro de código de erro em vez de lançar. Portanto, a biblioteca pode ser usada sem exceções, se assim desejar pela aplicação.
Todas as funções que possam falhar devido a um erro do sistema retornarão um resultado. Isso eliminará a necessidade do "último erro" e, portanto, a última variável de erro armazenada em cache na classe socket
desaparecerá. As classes de soquete envolverão apenas o identificador do soquete, tornando-as mais seguras para serem compartilhadas entre threads da mesma forma que um identificador pode ser compartilhado - normalmente com um thread para leitura e outro para gravação.
Alguns trabalhos também começaram a incorporar Secure Sockets em uma versão 2.x da biblioteca usando bibliotecas OpenSSL ou MbedTLS, ou (provavelmente), uma opção de tempo de construção para uma ou outra. PR #17, que ficou inativo por alguns anos, está sendo mesclado e atualizado, junto com um novo trabalho para fazer algo comparável ao OpenSSL. Você poderá escolher uma biblioteca segura ou outra ao construir sockpp
.
A versão 2.0 também passará para C++17 e CMake v3.12 ou posterior.
Para acompanhar os últimos anúncios deste projeto, siga-me em:
Mastodonte: @[email protected]
Twitter: @fmpagliughi
Se você estiver usando esta biblioteca, envie um tweet para mim ou me envie uma mensagem e me diga como você a está usando. Estou sempre curioso para ver onde isso vai parar!
A biblioteca, quando instalada, normalmente pode ser descoberta com find_package(sockpp)
. Ele usa o namespace Sockpp
e o nome da biblioteca sockpp
.
Um arquivo CMakeLists.txt simples pode ter esta aparência:
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)
As contribuições são aceitas e apreciadas. Trabalho novo e instável é feito no branch develop
. Envie todas as solicitações pull para esse branch, não para master .
Para obter mais informações, consulte: CONTRIBUTING.md
CMake é o sistema de compilação compatível.
Para construir com opções padrão:
$ cd sockpp
$ cmake -Bbuild .
$ cmake --build build/
Para instalar:
$ cmake --build build/ --target install
A biblioteca possui diversas opções de construção via CMake para escolher entre criar uma biblioteca estática ou compartilhada (dinâmica) – ou ambas. Também permite construir opções de exemplo e, se o Doxygen estiver instalado, ele pode ser usado para criar documentação.
Variável | Valor padrão | Descrição |
---|---|---|
SOCKPP_BUILD_SHARED | SOBRE | Se deve construir a biblioteca compartilhada |
SOCKPP_BUILD_STATIC | DESLIGADO | Se deve construir a biblioteca estática |
SOCKPP_BUILD_DOCUMENTATION | DESLIGADO | Crie e instale a documentação da API baseada em HTML (requer Doxygen) |
SOCKPP_BUILD_EXAMPLES | DESLIGADO | Crie programas de exemplo |
SOCKPP_BUILD_TESTS | DESLIGADO | Crie os testes de unidade (requer Catch2 ) |
SOCKPP_WITH_CAN | DESLIGADO | Inclui suporte para SocketCAN. (somente Linux) |
Defina-os usando a opção '-D' no comando de configuração do CMake. Por exemplo, para criar documentação e aplicativos de exemplo:
$ cd sockpp
$ cmake -Bbuild -DSOCKPP_BUILD_DOCUMENTATION=ON -DSOCKPP_BUILD_EXAMPLES=ON .
$ cmake --build build/
Para construir a biblioteca com suporte a soquete seguro, uma biblioteca TLS precisa ser escolhida para fornecer suporte. Atualmente OpenSSL ou MbedTLS podem ser usados.
Escolha uma das seguintes opções ao configurar a compilação:
Variável | Valor padrão | Descrição |
---|---|---|
SOCKPP_WITH_MBEDTLS | DESLIGADO | Soquetes seguros com MbedTLS |
SOCKPP_WITH_OPENSSL | DESLIGADO | Soquetes seguros com OpenSSL |
A biblioteca sockpp
atualmente suporta MbedTLS v3.3. Ao construir essa biblioteca, as seguintes opções de configuração devem ser definidas no arquivo de configuração, include/mbedtls/mbedtls_config.h
#define MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK
Para oferecer suporte ao encadeamento:
#define MBEDTLS_THREADING_PTHREAD
#define MBEDTLS_THREADING_C
e defina a opção de compilação do CMake:
LINK_WITH_PTHREAD:BOOL=ON
Observe que as opções no arquivo de configuração já devem estar presentes no arquivo, mas comentadas por padrão. Simplesmente descomente-os, salve e construa.
O wrapper sockpp
OpenSSL está sendo construído e testado com OpenSSL v3.0
O TCP e outros aplicativos de rede de "streaming" geralmente são configurados como servidores ou clientes. Um aceitador é usado para criar um servidor TCP/streaming. Ele vincula um endereço e escuta em uma porta conhecida para aceitar conexões de entrada. Quando uma conexão é aceita, um novo soquete de streaming é criado. Esse novo soquete pode ser tratado diretamente ou movido para um thread (ou pool de threads) para processamento.
Por outro lado, para criar um cliente TCP, um objeto conector é criado e conectado a um servidor em um endereço conhecido (normalmente host e soquete). Quando conectado, o soquete é de streaming que pode ser usado para ler e escrever diretamente.
Para IPv4 as classes tcp_acceptor
e tcp_connector
são usadas para criar servidores e clientes, respectivamente. Eles usam a classe inet_address
para especificar endereços de terminais compostos por um endereço de host de 32 bits e um número de porta de 16 bits.
tcp_acceptor
O tcp_acceptor
é usado para configurar um servidor e escutar conexões de entrada.
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();
O aceitador normalmente fica em um loop aceitando novas conexões e as transfere para outro processo, thread ou pool de threads para interagir com o cliente. No C++ padrão, isso poderia ser assim:
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();
}
}
Os perigos de um projeto de thread por conexão estão bem documentados, mas a mesma técnica pode ser usada para passar o soquete para um pool de threads, se houver algum disponível.
Veja o exemplo tcpechosvr.cpp.
tcp_connector
O cliente TCP é um pouco mais simples, pois um objeto tcp_connector
é criado e conectado e pode ser usado para ler e gravar dados diretamente.
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));
Veja o exemplo tcpecho.cpp.
udp_socket
Os soquetes UDP podem ser usados para comunicações sem conexão:
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);
Consulte os exemplos udpecho.cpp e udpechosvr.cpp.
O mesmo estilo de conectores e aceitadores pode ser usado para conexões TCP sobre IPv6 usando as classes:
inet6_address
tcp6_connector
tcp6_acceptor
tcp6_socket
udp6_socket
Os exemplos estão no diretório exemplos/tcp.
O mesmo se aplica à conexão local em sistemas *nix que implementam soquetes de domínio Unix. Para isso utilize as classes:
unix_address
unix_connector
unix_acceptor
unix_socket (unix_stream_socket)
unix_dgram_socket
Os exemplos estão no diretório exemplos/unix.
A Controller Area Network (barramento CAN) é um protocolo relativamente simples, normalmente usado por microcontroladores para comunicação dentro de um automóvel ou máquina industrial. O Linux possui o pacote SocketCAN que permite aos processos compartilhar o acesso a uma interface física de barramento CAN usando soquetes no espaço do usuário. Veja: Linux SocketCAN
No nível mais baixo, os dispositivos CAN escrevem pacotes individuais, chamados de "quadros", em endereços numéricos específicos no barramento.
Por exemplo, um dispositivo com sensor de temperatura pode ler a temperatura peroidicamente e gravá-la no barramento como um número inteiro bruto 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);
}
Um receptor para obter um quadro pode ser assim:
can_address addr("CAN0");
can_socket sock(addr);
can_frame frame;
sock.recv(&frame);
A hierarquia de classes de soquete é construída sobre uma classe base socket
. A maioria dos aplicativos simples provavelmente não usará socket
diretamente, mas sim classes derivadas definidas para uma família de endereços específica, como tcp_connector
e tcp_acceptor
.
Os objetos de soquete mantêm um identificador para um identificador de soquete do sistema operacional subjacente e um valor armazenado em cache para o último erro ocorrido nesse soquete. O identificador de soquete normalmente é um descritor de arquivo inteiro, com valores >=0 para soquetes abertos e -1 para um soquete não aberto ou inválido. O valor usado para soquetes não abertos é definido como uma constante, INVALID_SOCKET
, embora geralmente não precise ser testado diretamente, pois o próprio objeto será avaliado como falso se não for inicializado ou estiver em estado de erro. Uma verificação de erro típica seria assim:
tcp_connector conn({"localhost", 12345});
if (!conn)
cerr << conn.last_error_str() << std::endl;
Os construtores padrão para cada uma das classes de soquete não fazem nada e simplesmente definem o identificador subjacente como INVALID_SOCKET
. Eles não criam um objeto de soquete. A chamada para conectar ativamente um objeto connector
ou abrir um objeto acceptor
criará um soquete de sistema operacional subjacente e, em seguida, executará a operação solicitada.
Geralmente, um aplicativo pode executar a maioria das operações de baixo nível com a biblioteca. Soquetes desconectados e não vinculados podem ser criados com a função estática create()
na maioria das classes e, em seguida, vincular e escutar manualmente nesses soquetes.
O método socket::handle()
expõe o identificador do sistema operacional subjacente que pode então ser enviado para qualquer chamada de API da plataforma que não seja exposta pela biblioteca.
Um objeto de soquete não é seguro para threads. Os aplicativos que desejam ter vários threads lendo ou gravando em um soquete devem usar alguma forma de serialização, como std::mutex
para proteger o acesso.
Um socket
pode ser movido de um thread para outro com segurança. Este é um padrão comum para um servidor que usa um thread para aceitar conexões de entrada e então passa o novo soquete para outro thread ou pool de threads para manipulação. Isso pode ser feito 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));
Neste caso, handle_connection seria uma função que recebe um soquete por valor, como:
void handle_connection(sockpp::tcp6_socket sock) { ... }
Como um socket
não pode ser copiado, a única opção seria mover o soquete para uma função como esta.
É um padrão comum, especialmente em aplicativos clientes, ter um thread para ler em um soquete e outro thread para gravar no soquete. Nesse caso, o identificador do soquete subjacente pode ser considerado thread-safe (um thread de leitura e um thread de gravação). Mas mesmo nesse cenário, um objeto sockpp::socket
ainda não é thread-safe devido especialmente ao valor de erro armazenado em cache. O thread de gravação pode ver um erro que ocorreu no thread de leitura e vice-versa.
A solução para este caso é usar o método socket::clone()
para fazer uma cópia do soquete. Isso usará a função dup()
do sistema ou similar para criar outro soquete com uma cópia duplicada do identificador do soquete. Isto tem o benefício adicional de que cada cópia do soquete pode manter uma vida útil independente. O soquete subjacente não será fechado até que ambos os objetos saiam do escopo.
sockpp::tcp_connector conn({host, port});
auto rdSock = conn.clone();
std::thread rdThr(read_thread_func, std::move(rdSock));
O método socket::shutdown()
pode ser usado para comunicar a intenção de fechar o soquete de um desses objetos para o outro sem a necessidade de outro mecanismo de sinalização de thread.
Veja o exemplo tcpechomt.cpp.