如果单纯的从代码角度来看,IPV4和IPV6的变化基本不大,主要是围绕着sockaddr系列结构体的变化,API层面基本没变化,变化最大的就是将字符串解析成地址结构体的那几个函数,为了解析字符串中的IPV4地址,IPV4使用 inet_addr()函数,在升级之后,更改为inet_pton函数,这个函数除了地址字符串,还需要指定IP协议版本,另外,对域名的解析也有变化,IPV4使用gethostbyname()函数,而新的协议采用getaddrinfo()函数,只要处理好了这几个函数,IPV4 V6混合编码就基本OK了。
IPV4 IPV6混合编码最重要的就是理清sockaddr系列结构体的关系,主要有这几个结构体:
sockaddr // 最基本的结构体,所有的API都是用这个结构体指针,用结构体长度来判断使用哪个结构体;
sockaddr_in // IPV4使用的地址结构;
sockaddr_in6 // IPV6使用的地址结构;
sockaddr_storage // 通用的地址存储结构体,建议尽可能的用这个结构体来编码;
这儿给出一个解析地址的函数实现:
bool CSocketUtility::DomainAnalyze(tagDomain & domain)
{
bool result = false;
if( !domain.host.empty() && domain.data.GetCapacity() )
{
if( IsIPV4Address( domain.host.c_str() ) )
{
if( domain.data.GetCapacity() >= sizeof(sockaddr_in) &&
inet_pton( AF_INET, domain.host.c_str(), & (
domain.data.Query<sockaddr_in>() -> sin_addr ) ) > 0 )
{
// return 1, succeed.
domain.data.SetLength( sizeof(sockaddr_in) );
domain.data.Query<sockaddr_in>() -> sin_family = AF_INET;
domain.data.Query<sockaddr_in>() -> sin_port = htons( domain.port );
result = true;
}
}
else
if( IsIPV6Address( domain.host.c_str() ) )
{
if( domain.data.GetCapacity() >= sizeof(sockaddr_in6) &&
inet_pton( AF_INET6, domain.host.c_str(), & (
domain.data.Query<sockaddr_in6>() -> sin6_addr ) ) > 0 )
{
// return 1, succeed.
domain.data.SetLength( sizeof(sockaddr_in6) );
domain.data.Query<sockaddr_in6>() -> sin6_family = AF_INET6;
domain.data.Query<sockaddr_in6>() -> sin6_port = htons( domain.port );
result = true;
}
}
else
{
addrinfo hints;
memset( & hints, 0, sizeof( hints ) );
hints.ai_family = AF_UNSPEC;
addrinfo * retval = NULL;
if( 0 == getaddrinfo( domain.host.c_str(), NULL, & hints, & retval ) )
{
// return 0, succeeded.
if( retval )
{
for( addrinfo * current = retval;
!result && current != NULL; current = current -> ai_next )
{
switch( current -> ai_family )
{
case AF_UNSPEC:
// nothing to do here;
break;
case AF_INET:
if( current -> ai_addrlen <= domain.data.GetCapacity() ) {
memcpy( domain.data, current -> ai_addr, current -> ai_addrlen );
domain.data.SetLength( current -> ai_addrlen );
domain.data.Query<sockaddr_in>() -> sin_port = htons( domain.port );
result = true;
}
break;
case AF_INET6:
if( current -> ai_addrlen <= domain.data.GetCapacity() ) {
memcpy( domain.data, current -> ai_addr, current -> ai_addrlen );
domain.data.SetLength( current -> ai_addrlen );
domain.data.Query<sockaddr_in6>() -> sin6_port = htons( domain.port );
result = true;
}
break;
}
}
freeaddrinfo( retval );
retval = NULL;
}
}
}
}
return result;
}
其中,tagDomain就是地址的结构体,包含以下几个数据:
struct tagDomain
{
public:
a_string host;
U16 port;
CMemory data;
tagDomain();
tagDomain(const tagDomain & value);
virtual ~tagDomain();
tagDomain & operator=(const tagDomain & value);
}; //end struct tagDomain
其中,host是主机地址,port就是端口,而data,就是存储的sockaddr_storage,结构体初始化时,需要为data分配好内存;
在解析好地址之后,host, port, data内,都存储好了数据,并在sockaddr中,存储了地址所使用的协议版本,地址,端口,这个结构体如果是监听器就必须一直保存,如果是连接器,在连接之后就可以丢弃了。接下来的代码和原来几乎一样,只需做少量改变。
首先是创建SOCKET,需要根据协议版本创建对应的SOCKET类型。
SOCKET CTcpSelectModelImplement::Create(tagDomain & domain)
{
SOCKET result = INVALID_SOCKET;
if( domain.data.Alloc(
sizeof(sockaddr_storage) ) &&
CSocketUtility::DomainAnalyze(domain) )
{
result = socket(
domain.data.Query<sockaddr>() -> sa_family,
SOCK_STREAM, 0 );
}
return result;
}
注意这个函数,创建socket时用的是sockaddr结构体,无论sockaddr_in还是sockaddr_in6,结构体开头的版本成员变量都是一样的。可以用sockaddr这个结构体来取。
另外在开始的时候,解析地址之前,tagDomain 有一次内存的分配行为,分配的大小时sockaddr_storage的大小。
创建socket之后,就是不同的流程,监听器是绑定+监听,而连接器就是连接主机:
bool CTcpSelectModelImplement::Listen(U64 name, const tagDomain & domain)
{
bool result = false;
if( name && !domain.host.empty() &&
domain.port && domain.data.GetLength() )
{
CObjectPtr listener = Obtain( name );
if( listener )
{
SOCKET hs = INVALID_SOCKET;
CInterface<ITcpConnectionController> segment;
if( segment.Mount(listener, IID_TCP_CONNECTION_CONTROLLER) ) {
hs = segment -> GetSocket();
}
if( hs != INVALID_SOCKET )
{
// 绑定该地址上的该端口给hs。
if( SOCKET_ERROR != ::bind(
hs, domain.data.Query<sockaddr>(), domain.data.GetLength() ) ) {
result = ( listen(hs, 5) != SOCKET_ERROR );
}
if( result )
{
CCriticalSectionScope scope( region );
server.insert( make_pair(hs, name) );
server_update = true;
}
}
}
}
return result;
}
可以注意到在bind函数中,使用的就是sockaddr结构体指针,并且通过结构体的长度来判断采用那个协议版本。
连接函数如下:
bool CTcpSelectModelImplement::Connect(U64 name, const tagDomain & domain)
{
bool result = false;
if( name && domain.host.length() &&
domain.port && domain.data.GetLength() )
{
CObjectPtr connector = Obtain( name );
if( connector )
{
SOCKET hs = INVALID_SOCKET;
CInterface<ITcpConnectionController> segment;
if( segment.Mount(connector, IID_TCP_CONNECTION_CONTROLLER) ) {
hs = segment -> GetSocket();
}
if( hs != INVALID_SOCKET )
{
// connect to domain;
result = ( 0 == connect( hs,
domain.data.Query<sockaddr>(), domain.data.GetLength() ) );
if( result )
{
CCriticalSectionScope scope( region );
client.insert( make_pair(hs, name) );
client_update = true;
}
}
}
}
return result;
}
这个处理与监听函数基本一样,而接受,发送消息的函数更是没有什么需要更改的地方。
到此,IPV4 IPV6的混合编码基本就完成了,IPV4和IPV6最大的变化就是地址解析,而IO模型层面更是没有什么改变,没有必要为IPV4和IPV6分别写一个类来实现不同的协议版本,只需要在原类上稍微修改一下结构即可。
另外,需要注意的是,对IOCP这个IO模型,使用的不是标准的socket函数,有些函数需要注意:
result = ( AcceptEx( listener, hs, wb -> buf, 0,
sizeof(sockaddr_storage), sizeof(sockaddr_storage), & bytes, ol ) == TRUE );
在AcceptEx函数中,原来使用的是 sizeof(sockaddr_in),为了兼容IPV6,全部改为sizeof(sockaddr_storage),否则使用IPV6的时候,会得到内存不足的错误。