如果單純的從代碼角度來看,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的時候,會得到內存不足的錯誤。