Abaixo está um exemplo mínimo de funcionamento de um servidor C++ que implementa uma infraestrutura de rede de pilha dupla. (O que significa que um único soquete manipula conexões IPv4 e IPv6.)
#include <format>
#include <print>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <signal.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
const auto SERVER_PORT = 7778;
const auto server_fd = socket(AF_INET6, SOCK_STREAM, 0);
int opt = 0;
setsockopt(server_fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
sockaddr_in6 server_address;
std::memset(&server_address, 0, sizeof(server_address));
server_address.sin6_family = AF_INET6;
server_address.sin6_addr = in6addr_any;
server_address.sin6_port = htons(SERVER_PORT);
bind(server_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
listen(server_fd, 10);
sockaddr_storage peer_address;
socklen_t peer_address_length = sizeof(peer_address);
auto peer_fd = accept(server_fd, reinterpret_cast<sockaddr*>(&peer_address), &peer_address_length);
if(peer_address.ss_family == AF_INET)
{
const auto p_peer_address = &peer_address;
sockaddr_in* ipv4 = (sockaddr_in*)p_peer_address;
std::println("Client port (IPv4): {}", ntohs(ipv4->sin_port));
}
else if(peer_address.ss_family == AF_INET6)
{
const auto p_peer_address = &peer_address;
sockaddr_in6* ipv6 = (sockaddr_in6*)p_peer_address;
std::println("Client port (IPv6): {}", ntohs(ipv6->sin6_port));
}
else
{
throw std::runtime_error("unrecognized ss_family");
}
close(peer_fd);
close(server_fd);
return 0;
}
No entanto, não está funcionando como esperado. Independentemente de um cliente se conectar via ipv4 ou ipv6, a lógica sempre segue o else if
branch da if
declaração. O primeiro if
branch nunca é executado, indicando que nenhuma conexão é iniciada com a AF_INET
família.
Ambos os clientes parecem realmente funcionar. O servidor responde quando uma conexão é iniciada - é só que a mensagem ipv6 é impressa independentemente do tipo de conexão do cliente. (ipv4 ou ipv6)
Se fizer diferença, os clientes e o servidor estão sendo executados na mesma máquina, conectando-se via localhost ( 127.0.0.1
)
Exemplos de código C++ para ambos os tipos de cliente são fornecidos abaixo.
// Client: ipv6
#include <format>
#include <print>
#include <format>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
const auto PORT = 7778;
const auto socket_fd = socket(AF_INET6, SOCK_STREAM, 0);
in6_addr server_sin6_address;
std::memset(&server_sin6_address, 0, sizeof(server_sin6_address));
inet_pton(AF_INET6, "::1", &server_sin6_address);
sockaddr_storage server_address;
std::memset(&server_address, 0, sizeof(server_address));
server_address.ss_family = AF_INET6;
sockaddr_storage *p_server_address = &server_address;
sockaddr_in6 *p_server_address_in6 = reinterpret_cast<sockaddr_in6*>(p_server_address);
p_server_address_in6->sin6_family = AF_INET6; // why repeat?
p_server_address_in6->sin6_port = htons(PORT);
p_server_address_in6->sin6_flowinfo = 0; // not used?
p_server_address_in6->sin6_addr = server_sin6_address;
p_server_address_in6->sin6_scope_id = 0; // not used?
const auto connect_result = connect(socket_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
const char* const buffer = "hello world ipv6";
const auto buffer_length = strlen(buffer) + 1;
send(socket_fd, buffer, buffer_length, 0);
close(socket_fd);
return 0;
}
// Client: ipv4
#include <format>
#include <print>
#include <format>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
const auto PORT = 7778;
const auto socket_fd = socket(AF_INET, SOCK_STREAM, 0);
in_addr server_sin_address;
std::memset(&server_sin_address, 0, sizeof(server_sin_address));
inet_pton(AF_INET, "127.0.0.1", &server_sin_address);
sockaddr_storage server_address;
std::memset(&server_address, 0, sizeof(server_address));
server_address.ss_family = AF_INET;
sockaddr_storage *p_server_address = &server_address;
sockaddr_in *p_server_address_in = reinterpret_cast<sockaddr_in*>(p_server_address);
p_server_address_in->sin_family = AF_INET; // why repeat?
p_server_address_in->sin_port = htons(PORT);
p_server_address_in->sin_addr = server_sin_address;
//p_server_address_in->sin_zero = 0; // not used?
const auto connect_result = connect(socket_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
const char* const buffer = "hello world ipv4";
const auto buffer_length = strlen(buffer) + 1;
const auto send_result = send(socket_fd, buffer, buffer_length, 0);
close(socket_fd);
std::println("Server quit");
return 0;
}
Talvez seja um bug na implementação do cliente IPv4, mas até agora não encontrei nada que parecesse estar obviamente incorreto.
As conexões ipv4 para um destino ipv6 de alguma forma são atualizadas automaticamente pelo SO ou algo assim? Parece um comportamento muito estranho.
Um dual-stack socket é um socket IPv6 que pode se comunicar com um peer IPv4 (desabilitando a
IPV6_V6ONLY
opção). Ainda assim, é um socket IPv6.Um
AF_INET
socket pode funcionar somente comAF_INET
endereços. E, da mesma forma, umAF_INET6
socket pode funcionar somente comAF_INET6
endereços. Então, quando um cliente é aceito por umAF_INET6
servidor, seja o servidor dual-stack ou não, o socket aceito também seráAF_INET6
. Que é o que você está vendo acontecer.Mas, para um servidor dual-stack, se o cliente estiver usando IPv4 em vez de IPv6, então o endereço IP do cliente que é relatado por
accept()
ougetpeername()
será um endereço IPv6 mapeado para IPv4 , ou seja, umAF_INET6
endereço que tem um prefixo de 96 bits0:0:0:0:0:FFFF
e os 32 bits restantes serão o endereço IPv4.Você pode ver isso se registrar os IPs reais, não apenas as portas.
Por exemplo: