下面是一个实现双栈网络基础设施的 C++ 服务器的最小工作示例。(这意味着单个套接字可同时处理 ipv4 和 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;
}
但是,它并没有按预期工作。无论客户端是通过 ipv4 还是 ipv6 进行连接,逻辑始终遵循语句else if
的分支if
。第一个if
分支从未运行,表示没有与该系列建立任何连接AF_INET
。
两个客户端似乎都可以正常工作。当连接启动时,服务器会做出响应 - 无论客户端连接类型如何,都会打印 ipv6 消息。(ipv4 或 ipv6)
如果有什么区别的话,客户端和服务器都在同一台机器上运行,通过 localhost 连接(127.0.0.1
)
下面提供了两种客户端类型的示例 C++ 代码。
// 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;
}
也许这是 IPv4 客户端实现中的一个错误,但到目前为止我还没有发现任何看起来明显不正确的东西。
到 ipv6 目标的 ipv4 连接是否会以某种方式由操作系统自动升级?这似乎是一种非常奇怪的行为。
双栈套接字是可以与 IPv4 对等体通信的 IPv6 套接字(通过禁用该
IPV6_V6ONLY
选项)。尽管如此,它仍然是 IPv6 套接字。套接字
AF_INET
只能使用AF_INET
地址。同样,AF_INET6
套接字只能使用AF_INET6
地址。因此,当客户端被服务器接受时AF_INET6
,无论服务器是否是双栈的,接受的套接字也将是AF_INET6
。这就是您看到的情况。但是,对于双栈服务器,如果客户端使用的是 IPv4 而不是 IPv6,那么客户端报告的 IP 地址
accept()
将getpeername()
是IPv4 映射的 IPv6 地址,即AF_INET6
具有 96 位前缀的地址0:0:0:0:0:FFFF
,其余 32 位将是 IPv4 地址。如果您记录实际 IP 而不仅仅是端口,您就可以看到这一点。
例如: