【发布时间】:2021-05-17 19:59:56
【问题描述】:
下面的代码是我在 C++ 中的最小跨平台回显服务器的尝试。 它符合我的预期,除了依赖于平台的不对称性 处理 IP 向后兼容性:在 Windows 上,与我尝试过的其他两个平台不同,IPv6 服务器无法处理来自 IPv4 客户端的请求。*
对我来说,其他平台可以做到这一点(下面的结果#3)是一个意外但非常受欢迎的胜利。它为我开辟了一些可能性,前提是我也可以让它在 Windows 上工作。所以我的问题是:是否预计同样的事情在 Windows 上会失败(比较结果 #3 和 #5)?我可以用服务器代码做些什么来让#5 成功吗?
分解如下:
服务器使用 IPv4 (minimal_echo_server 8081 4):
- 客户端使用 IPv4 地址:服务器按预期响应
- 客户端使用 IPv6 地址:连接失败** 符合预期
服务器在 Ubuntu 20.04 或 macOS 10.13 (./minimal_echo_server 8081 6) 上使用 IPv6:
- 客户端使用 IPv4 地址:服务器响应正常(将客户端的 IP 呈现为
::ffff:后跟 IPv4 地址) - 客户端使用 IPv6 地址:服务器按预期响应
服务器在 Windows 10 上使用 IPv6 (.\minimal_echo_server.exe 8081 6):
- 客户端使用 IPv4 地址:连接失败**
- 客户端使用 IPv6 地址:服务器按预期响应
* 我所说的“IPv4 客户端”的一个例子可能是以下netcat 调用:
nc WWW.XXX.YYY.ZZZ 8081 # target the server via its IPv4 address
与以下客户端使用 IPv6 的情况相比:
nc fe80::WWWW:XXXX:YYYY:ZZZZ%en0 8081 # target the server via its IPv6 link-local address (with the client's local adapter name appended as scope)
** 在两种“失败”情况下,失败模式似乎取决于客户端操作系统:Windows 10 客户端会暂停几秒钟,然后报告“无法建立连接”因为目标机器主动拒绝了它”,而据我所知,我的 Linux 和 Darwin 客户端无限期挂起。
// Welcome to minimal_echo_server.cpp
// This can be compiled with `cl.exe minimal_echo_server.cpp` on Windows 10 using Visual Studio 2019
// or with `g++ -o minimal_echo_server minimal_echo_server.cpp` on something more GNUey.
#ifdef _WIN32
# pragma comment(lib, "Ws2_32.lib") // winsock2
# include <winsock2.h>
# include <ws2tcpip.h> // for inet_pton() and inet_ntop()
# define SOCKET_IS_VALID(S) (S != INVALID_SOCKET)
# define CLOSE_SOCKET closesocket
typedef int socklen_t;
bool InitializeSockets(void)
{
static bool initialized = false;
WSADATA wsa;
if(!initialized && WSAStartup(MAKEWORD(2, 2), &wsa) == 0) initialized = true;
return initialized;
}
#else
# include <sys/socket.h> // for socket(), bind(), etc
# include <arpa/inet.h> // for sockaddr_in and inet_ntop()
# include <unistd.h> // close()
# define SOCKET_IS_VALID(S) (S >= 0)
# define CLOSE_SOCKET close
typedef int SOCKET;
bool InitializeSockets(void) { return true; }
#endif
#include <string.h>
#include <string>
#include <sstream>
#include <iostream>
#define USAGE "Mandatory first argument: port number (decimal integer > 0)\n" \
"Optional second argument: 4 or 6 to denote IP version (default: 4)\n"
int main(int argc, const char * argv[])
{
const int maxPendingConnections = 5;
struct ::sockaddr_in serverAddress4, remoteAddress4;
struct ::sockaddr_in6 serverAddress6, remoteAddress6;
struct ::sockaddr * addressPtr;
::socklen_t addressSize;
int domain;
int port = (argc > 1) ? ::atoi(argv[1]) : 0;
int ipVersion = (argc > 2) ? ::atoi(argv[2]) : 4;
if(!port) { std::cerr << USAGE; return -1; }
if(!InitializeSockets()) return -2;
if(ipVersion == 4)
{
domain = PF_INET;
addressPtr = (struct ::sockaddr *)&serverAddress4;
addressSize = (::socklen_t)sizeof(serverAddress4);
::memset(addressPtr, 0, addressSize);
serverAddress4.sin_family = AF_INET;
serverAddress4.sin_port = htons(port);
serverAddress4.sin_addr.s_addr = htonl(INADDR_ANY);
}
else if(ipVersion == 6)
{
domain = PF_INET6;
addressPtr = (struct ::sockaddr *)&serverAddress6;
addressSize = (::socklen_t)sizeof(serverAddress6);
::memset(addressPtr, 0, addressSize);
serverAddress6.sin6_family = AF_INET6;
serverAddress6.sin6_port = htons(port);
serverAddress6.sin6_addr = in6addr_any;
serverAddress6.sin6_scope_id = 0; // right?
}
else { std::cerr << USAGE; return -1; }
SOCKET localServerSocket = ::socket(domain, SOCK_STREAM, IPPROTO_TCP);
if(!SOCKET_IS_VALID(localServerSocket)) return -3;
// to keep the example minimal, I will not be explicitly closing sockets on error
if(::bind(localServerSocket, addressPtr, addressSize) != 0) return -4;
if(::listen(localServerSocket, maxPendingConnections) != 0) return -5;
std::cerr << "listening on port " << port << " using IPv" << ipVersion << std::endl;
while(true)
{
if(ipVersion == 4)
{
addressPtr = (struct ::sockaddr *)&remoteAddress4;
addressSize = (::socklen_t)sizeof(remoteAddress4);
}
else if(ipVersion == 6)
{
addressPtr = (struct ::sockaddr *)&remoteAddress6;
addressSize = (::socklen_t)sizeof(remoteAddress6);
}
SOCKET remoteConnectionSocket = ::accept(localServerSocket, addressPtr, &addressSize);
if(!SOCKET_IS_VALID(remoteConnectionSocket)) return -6;
char buffer[128];
std::string remoteAddressString;
if(ipVersion == 4)
{
remoteAddressString = ::inet_ntop(AF_INET, &remoteAddress4.sin_addr, buffer, (socklen_t)sizeof(buffer)) ? buffer : "???";
}
else if(ipVersion == 6)
{
remoteAddressString = ::inet_ntop(AF_INET6, &remoteAddress6.sin6_addr, buffer, (socklen_t)sizeof(buffer)) ? buffer : "???";
if( IN6_IS_ADDR_LINKLOCAL( &remoteAddress6.sin6_addr ) )
{
std::stringstream ss;
ss << "%" << remoteAddress6.sin6_scope_id;
remoteAddressString += ss.str();
}
}
std::cerr << " accepted connection from " << remoteAddressString << std::endl;
while(true)
{
char incomingData[32];
int bytesReceived = ::recv(remoteConnectionSocket, incomingData, sizeof(incomingData), 0);
if(bytesReceived < 0) return -7;
if(bytesReceived == 0) break;
std::cerr << " received " << bytesReceived << " bytes from " << remoteAddressString << std::endl;
int bytesSent = ::send(remoteConnectionSocket, incomingData, bytesReceived, 0); // echo
if(bytesSent != bytesReceived) return -8;
}
CLOSE_SOCKET(remoteConnectionSocket);
std::cerr << " closed connection from " << remoteAddressString << std::endl;
}
return 0; // never reached, but let's suppress the compiler warning
}
【问题讨论】:
-
请注意,
boostnetworking facilities 应该易于设置,以便以跨平台的方式正确处理所有这些。 -
IPv6不向后兼容 IPv4。 IPv4 和 IPv6 是独立的、不兼容的协议,仅 IPv4 的主机不能直接与仅 IPv6 的主机通信,反之亦然。
标签: c++ cross-platform ipv6 ipv4